From e396fe054e66518d0587f9814a38682c11b4bde8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 26 Mar 2020 14:40:06 +0100 Subject: [PATCH 001/103] Refactored Resource Policy service --- src/app/core/core.module.ts | 4 +- .../core/data/resource-policy.service.spec.ts | 75 ----- .../models/action-type.model.ts | 21 +- .../models/policy-type.model.ts | 25 ++ .../models/resource-policy.model.ts | 85 ++++++ .../models}/resource-policy.resource-type.ts | 4 +- .../resource-policy.service.spec.ts | 263 ++++++++++++++++++ .../resource-policy.service.ts | 73 ++++- src/app/core/shared/collection.model.ts | 4 +- src/app/core/shared/resource-policy.model.ts | 58 ---- ...tion-upload-access-conditions.component.ts | 2 +- .../upload/section-upload.component.spec.ts | 9 +- .../upload/section-upload.component.ts | 6 +- 13 files changed, 464 insertions(+), 165 deletions(-) delete mode 100644 src/app/core/data/resource-policy.service.spec.ts rename src/app/core/{cache => resource-policy}/models/action-type.model.ts (75%) create mode 100644 src/app/core/resource-policy/models/policy-type.model.ts create mode 100644 src/app/core/resource-policy/models/resource-policy.model.ts rename src/app/core/{shared => resource-policy/models}/resource-policy.resource-type.ts (52%) create mode 100644 src/app/core/resource-policy/resource-policy.service.spec.ts rename src/app/core/{data => resource-policy}/resource-policy.service.ts (52%) delete mode 100644 src/app/core/shared/resource-policy.model.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 96f8a078d1..66089d2928 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -84,7 +84,7 @@ import { RegistryMetadatafieldsResponseParsingService } from './data/registry-me import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service'; import { RelationshipTypeService } from './data/relationship-type.service'; import { RelationshipService } from './data/relationship.service'; -import { ResourcePolicyService } from './data/resource-policy.service'; +import { ResourcePolicyService } from './resource-policy/resource-policy.service'; import { SearchResponseParsingService } from './data/search-response-parsing.service'; import { SiteDataService } from './data/site-data.service'; import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; @@ -123,7 +123,7 @@ import { RelationshipType } from './shared/item-relationships/relationship-type. import { Relationship } from './shared/item-relationships/relationship.model'; import { Item } from './shared/item.model'; import { License } from './shared/license.model'; -import { ResourcePolicy } from './shared/resource-policy.model'; +import { ResourcePolicy } from './resource-policy/models/resource-policy.model'; import { SearchConfigurationService } from './shared/search/search-configuration.service'; import { SearchFilterService } from './shared/search/search-filter.service'; import { SearchService } from './shared/search/search.service'; diff --git a/src/app/core/data/resource-policy.service.spec.ts b/src/app/core/data/resource-policy.service.spec.ts deleted file mode 100644 index abed805ca3..0000000000 --- a/src/app/core/data/resource-policy.service.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { cold, getTestScheduler } from 'jasmine-marbles'; -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 { ResourcePolicy } from '../shared/resource-policy.model'; -import { RequestService } from './request.service'; -import { ResourcePolicyService } from './resource-policy.service'; - -describe('ResourcePolicyService', () => { - let scheduler: TestScheduler; - let service: ResourcePolicyService; - let requestService: RequestService; - let rdbService: RemoteDataBuildService; - let objectCache: ObjectCacheService; - const testObject = { - uuid: '664184ee-b254-45e8-970d-220e5ccc060b' - } as ResourcePolicy; - const requestURL = `https://rest.api/rest/api/resourcepolicies/${testObject.uuid}`; - const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; - - beforeEach(() => { - scheduler = getTestScheduler(); - - requestService = jasmine.createSpyObj('requestService', { - generateRequestId: requestUUID, - configure: true - }); - rdbService = jasmine.createSpyObj('rdbService', { - buildSingle: cold('a', { - a: { - payload: testObject - } - }) - }); - objectCache = {} as ObjectCacheService; - const halService = {} as HALEndpointService; - const notificationsService = {} as NotificationsService; - const http = {} as HttpClient; - const comparator = {} as any; - - service = new ResourcePolicyService( - requestService, - rdbService, - objectCache, - halService, - notificationsService, - http, - comparator - ); - - spyOn((service as any).dataService, 'findByHref').and.callThrough(); - }); - - describe('findByHref', () => { - it('should proxy the call to dataservice.findByHref', () => { - scheduler.schedule(() => service.findByHref(requestURL)); - scheduler.flush(); - - expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL); - }); - - it('should return a RemoteData for the object with the given URL', () => { - const result = service.findByHref(requestURL); - const expected = cold('a', { - a: { - payload: testObject - } - }); - expect(result).toBeObservable(expected); - }); - }); -}); diff --git a/src/app/core/cache/models/action-type.model.ts b/src/app/core/resource-policy/models/action-type.model.ts similarity index 75% rename from src/app/core/cache/models/action-type.model.ts rename to src/app/core/resource-policy/models/action-type.model.ts index 4965f93e89..93c69c3705 100644 --- a/src/app/core/cache/models/action-type.model.ts +++ b/src/app/core/resource-policy/models/action-type.model.ts @@ -5,27 +5,27 @@ export enum ActionType { /** * Action of reading, viewing or downloading something */ - READ = 0, + READ = 'READ', /** * Action of modifying something */ - WRITE = 1, + WRITE = 'WRITE', /** * Action of deleting something */ - DELETE = 2, + DELETE = 'DELETE', /** * Action of adding something to a container */ - ADD = 3, + ADD = 'ADD', /** * Action of removing something from a container */ - REMOVE = 4, + REMOVE = 'REMOVE', /** * Action of performing workflow step 1 @@ -50,15 +50,20 @@ export enum ActionType { /** * Default Read policies for Bitstreams submitted to container */ - DEFAULT_BITSTREAM_READ = 9, + DEFAULT_BITSTREAM_READ = 'DEFAULT_BITSTREAM_READ', /** * Default Read policies for Items submitted to container */ - DEFAULT_ITEM_READ = 10, + DEFAULT_ITEM_READ = 'DEFAULT_ITEM_READ', /** * Administrative actions */ - ADMIN = 11, + ADMIN = 'ADMIN', + + /** + * Action of withdrawn reading + */ + WITHDRAWN_READ = 'WITHDRAWN_READ' } diff --git a/src/app/core/resource-policy/models/policy-type.model.ts b/src/app/core/resource-policy/models/policy-type.model.ts new file mode 100644 index 0000000000..21193e5ce5 --- /dev/null +++ b/src/app/core/resource-policy/models/policy-type.model.ts @@ -0,0 +1,25 @@ +/** + * Enum representing the Policy Type of a Resource Policy + */ +export enum PolicyType { + /** + * A policy in place during the submission + */ + TYPE_SUBMISSION = 'TYPE_SUBMISSION', + + /** + * A policy in place during the approval workflow + */ + TYPE_WORKFLOW = 'TYPE_WORKFLOW', + + /** + * A policy that has been inherited from a container (the collection) + */ + TYPE_INHERITED = 'TYPE_INHERITED', + + /** + * A policy defined by the user during the submission or workflow phase + */ + TYPE_CUSTOM = 'TYPE_CUSTOM', + +} diff --git a/src/app/core/resource-policy/models/resource-policy.model.ts b/src/app/core/resource-policy/models/resource-policy.model.ts new file mode 100644 index 0000000000..cf040867b0 --- /dev/null +++ b/src/app/core/resource-policy/models/resource-policy.model.ts @@ -0,0 +1,85 @@ +import { autoserialize, deserialize, deserializeAs } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; +import { ActionType } from './action-type.model'; +import { CacheableObject } from '../../cache/object-cache.reducer'; +import { HALLink } from '../../shared/hal-link.model'; +import { RESOURCE_POLICY } from './resource-policy.resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { ResourceType } from '../../shared/resource-type'; +import { PolicyType } from './policy-type.model'; + +/** + * Model class for a Resource Policy + */ +@typedObject +export class ResourcePolicy implements CacheableObject { + static type = RESOURCE_POLICY; + + /** + * The identifier for this Resource Policy + */ + @autoserialize + id: string; + + /** + * The name for this Resource Policy + */ + @autoserialize + name: string; + + /** + * The description for this Resource Policy + */ + @autoserialize + description: string; + + /** + * The classification or this Resource Policy + */ + @autoserialize + policyType: PolicyType; + + /** + * The action that is allowed by this Resource Policy + */ + @autoserialize + action: ActionType; + + /** + * The first day of validity of the policy (format YYYY-MM-DD) + */ + @autoserialize + startDate: string; + + /** + * The last day of validity of the policy (format YYYY-MM-DD) + */ + @autoserialize + endDate: string; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The universally unique identifier for this Resource Policy + * This UUID is generated client-side and isn't used by the backend. + * It is based on the ID, so it will be the same for each refresh. + */ + @deserializeAs(new IDToUUIDSerializer('resource-policy'), 'id') + uuid: string; + + /** + * The {@link HALLink}s for this ResourcePolicy + */ + @deserialize + _links: { + eperson: HALLink, + group: HALLink, + self: HALLink, + } +} diff --git a/src/app/core/shared/resource-policy.resource-type.ts b/src/app/core/resource-policy/models/resource-policy.resource-type.ts similarity index 52% rename from src/app/core/shared/resource-policy.resource-type.ts rename to src/app/core/resource-policy/models/resource-policy.resource-type.ts index 1811a3a0d1..d8ff3b9485 100644 --- a/src/app/core/shared/resource-policy.resource-type.ts +++ b/src/app/core/resource-policy/models/resource-policy.resource-type.ts @@ -1,4 +1,4 @@ -import { ResourceType } from './resource-type'; +import { ResourceType } from '../../shared/resource-type'; /** * The resource type for ResourcePolicy @@ -6,4 +6,4 @@ import { ResourceType } from './resource-type'; * Needs to be in a separate file to prevent circular * dependencies in webpack. */ -export const RESOURCE_POLICY = new ResourceType('resourcePolicy'); +export const RESOURCE_POLICY = new ResourceType('resourcepolicy'); diff --git a/src/app/core/resource-policy/resource-policy.service.spec.ts b/src/app/core/resource-policy/resource-policy.service.spec.ts new file mode 100644 index 0000000000..a2bfb52e01 --- /dev/null +++ b/src/app/core/resource-policy/resource-policy.service.spec.ts @@ -0,0 +1,263 @@ +import { HttpClient } from '@angular/common/http'; + +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +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 { ResourcePolicyService } from './resource-policy.service'; +import { PolicyType } from './models/policy-type.model'; +import { ActionType } from './models/action-type.model'; +import { FindListOptions } from '../data/request.models'; +import { SearchParam } from '../cache/models/search-param.model'; +import { PageInfo } from '../shared/page-info.model'; +import { PaginatedList } from '../data/paginated-list'; +import { createSuccessfulRemoteDataObject } from '../../shared/testing/utils'; +import { RequestEntry } from '../data/request.reducer'; +import { RestResponse } from '../cache/response.models'; + +describe('ResourcePolicyService', () => { + let scheduler: TestScheduler; + let service: ResourcePolicyService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + + const resourcePolicy = { + id: '1', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.READ, + startDate : null, + endDate : null, + type: 'resourcepolicy', + uuid: 'resource-policy-1', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + } + }; + + const anotherResourcePolicy = { + id: '2', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.WRITE, + startDate : null, + endDate : null, + type: 'resourcepolicy', + uuid: 'resource-policy-2', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + } + }; + const endpointURL = `https://rest.api/rest/api/resourcepolicies`; + const requestURL = `https://rest.api/rest/api/resourcepolicies/${resourcePolicy.id}`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + const resourcePolicyId = '1'; + const epersonUUID = '8b39g7ya-5a4b-438b-9686-be1d5b4a1c5a'; + const groupUUID = '8b39g7ya-5a4b-36987-9686-be1d5b4a1c5a'; + const resourceUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a'; + + const pageInfo = new PageInfo(); + const array = [resourcePolicy, anotherResourcePolicy ]; + const paginatedList = new PaginatedList(pageInfo, array); + const resourcePolicyRD = createSuccessfulRemoteDataObject(resourcePolicy); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + const responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + beforeEach(() => { + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: endpointURL }) + }); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: hot('a|', { + a: resourcePolicyRD + }), + buildList: hot('a|', { + a: paginatedListRD + }), + }); + objectCache = {} as ObjectCacheService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + + service = new ResourcePolicyService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + http, + comparator + ); + + spyOn((service as any).dataService, 'findById').and.callThrough(); + spyOn((service as any).dataService, 'findByHref').and.callThrough(); + spyOn((service as any).dataService, 'searchBy').and.callThrough(); + spyOn((service as any).dataService, 'getSearchByHref').and.returnValue(observableOf(requestURL)); + }); + + describe('findById', () => { + it('should proxy the call to dataservice.findById', () => { + scheduler.schedule(() => service.findById(resourcePolicyId)); + scheduler.flush(); + + expect((service as any).dataService.findById).toHaveBeenCalledWith(resourcePolicyId); + }); + + it('should return a RemoteData for the object with the given id', () => { + const result = service.findById(resourcePolicyId); + const expected = cold('a|', { + a: resourcePolicyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('findByHref', () => { + it('should proxy the call to dataservice.findByHref', () => { + scheduler.schedule(() => service.findByHref(requestURL)); + scheduler.flush(); + + expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.findByHref(requestURL); + const expected = cold('a|', { + a: resourcePolicyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('searchByEPerson', () => { + it('should proxy the call to dataservice.searchBy', () => { + const options = new FindListOptions(); + options.searchParams = [new SearchParam('uuid', epersonUUID)]; + scheduler.schedule(() => service.searchByEPerson(epersonUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByEPersonMethod, options); + }); + + it('should proxy the call to dataservice.searchBy with additional search param', () => { + const options = new FindListOptions(); + options.searchParams = [ + new SearchParam('uuid', epersonUUID), + new SearchParam('resource', resourceUUID), + ]; + scheduler.schedule(() => service.searchByEPerson(epersonUUID, resourceUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByEPersonMethod, options); + }); + + it('should return a RemoteData) for the search', () => { + const result = service.searchByEPerson(epersonUUID, resourceUUID); + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('searchByGroup', () => { + it('should proxy the call to dataservice.searchBy', () => { + const options = new FindListOptions(); + options.searchParams = [new SearchParam('uuid', groupUUID)]; + scheduler.schedule(() => service.searchByGroup(groupUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options); + }); + + it('should proxy the call to dataservice.searchBy with additional search param', () => { + const options = new FindListOptions(); + options.searchParams = [ + new SearchParam('uuid', groupUUID), + new SearchParam('resource', resourceUUID), + ]; + scheduler.schedule(() => service.searchByGroup(groupUUID, resourceUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options); + }); + + it('should return a RemoteData) for the search', () => { + const result = service.searchByGroup(groupUUID); + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('searchByResource', () => { + it('should proxy the call to dataservice.searchBy', () => { + const options = new FindListOptions(); + options.searchParams = [new SearchParam('uuid', resourceUUID)]; + scheduler.schedule(() => service.searchByResource(resourceUUID)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByResourceMethod, options); + }); + + it('should proxy the call to dataservice.searchBy with additional search param', () => { + const action = ActionType.READ; + const options = new FindListOptions(); + options.searchParams = [ + new SearchParam('uuid', resourceUUID), + new SearchParam('action', action), + ]; + scheduler.schedule(() => service.searchByResource(resourceUUID, action)); + scheduler.flush(); + + expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByResourceMethod, options); + }); + + it('should return a RemoteData) for the search', () => { + const result = service.searchByResource(resourceUUID); + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); +}); diff --git a/src/app/core/data/resource-policy.service.ts b/src/app/core/resource-policy/resource-policy.service.ts similarity index 52% rename from src/app/core/data/resource-policy.service.ts rename to src/app/core/resource-policy/resource-policy.service.ts index f66032925e..e79f04eb6f 100644 --- a/src/app/core/data/resource-policy.service.ts +++ b/src/app/core/resource-policy/resource-policy.service.ts @@ -11,16 +11,19 @@ import { RequestService } from '../data/request.service'; import { FindListOptions } from '../data/request.models'; import { Collection } from '../shared/collection.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ResourcePolicy } from '../shared/resource-policy.model'; +import { ResourcePolicy } from './models/resource-policy.model'; import { RemoteData } from '../data/remote-data'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { RESOURCE_POLICY } from '../shared/resource-policy.resource-type'; -import { ChangeAnalyzer } from './change-analyzer'; +import { RESOURCE_POLICY } from './models/resource-policy.resource-type'; +import { ChangeAnalyzer } from '../data/change-analyzer'; import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; -import { PaginatedList } from './paginated-list'; +import { PaginatedList } from '../data/paginated-list'; +import { ActionType } from './models/action-type.model'; +import { SearchParam } from '../cache/models/search-param.model'; +import { isNotEmpty } from '../../shared/empty.util'; /* tslint:disable:max-classes-per-file */ @@ -51,6 +54,9 @@ class DataServiceImpl extends DataService { @dataService(RESOURCE_POLICY) export class ResourcePolicyService { private dataService: DataServiceImpl; + protected searchByEPersonMethod = 'eperson'; + protected searchByGroupMethod = 'group'; + protected searchByResourceMethod = 'resource'; constructor( protected requestService: RequestService, @@ -74,13 +80,13 @@ export class ResourcePolicyService { } /** - * Returns a list of observables of {@link RemoteData} of {@link ResourcePolicy}s, based on an href, with a list of {@link FollowLinkConfig}, - * to automatically resolve {@link HALLink}s of the {@link ResourcePolicy} - * @param href The url of the {@link ResourcePolicy} we want to retrieve + * Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on its ID, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param id ID of {@link ResourcePolicy} we want to retrieve * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow); + findById(id: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findById(id, ...linksToFollow); } /** @@ -92,4 +98,53 @@ export class ResourcePolicyService { getDefaultAccessConditionsFor(collection: Collection, findListOptions?: FindListOptions): Observable>> { return this.dataService.findAllByHref(collection._links.defaultAccessConditions.href, findListOptions); } + + /** + * Return the {@link ResourcePolicy} list for a {@link EPerson} + * + * @param UUID UUID of a given {@link EPerson} + * @param resourceUUID Limit the returned policies to the specified DSO + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + searchByEPerson(UUID: string, resourceUUID?: string, ...linksToFollow: Array>) { + const options = new FindListOptions(); + options.searchParams = [new SearchParam('uuid', UUID)]; + if (isNotEmpty(resourceUUID)) { + options.searchParams.push(new SearchParam('resource', resourceUUID)) + } + return this.dataService.searchBy(this.searchByEPersonMethod, options, ...linksToFollow) + } + + /** + * Return the {@link ResourcePolicy} list for a {@link Group} + * + * @param UUID UUID of a given {@link Group} + * @param resourceUUID Limit the returned policies to the specified DSO + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + searchByGroup(UUID: string, resourceUUID?: string, ...linksToFollow: Array>) { + const options = new FindListOptions(); + options.searchParams = [new SearchParam('uuid', UUID)]; + if (isNotEmpty(resourceUUID)) { + options.searchParams.push(new SearchParam('resource', resourceUUID)) + } + return this.dataService.searchBy(this.searchByGroupMethod, options, ...linksToFollow) + } + + /** + * Return the {@link ResourcePolicy} list for a given DSO + * + * @param UUID UUID of a given DSO + * @param action Limit the returned policies to the specified {@link ActionType} + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + searchByResource(UUID: string, action?: ActionType, ...linksToFollow: Array>) { + const options = new FindListOptions(); + options.searchParams = [new SearchParam('uuid', UUID)]; + if (isNotEmpty(action)) { + options.searchParams.push(new SearchParam('action', action)) + } + return this.dataService.searchBy(this.searchByResourceMethod, options, ...linksToFollow) + } + } diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index ba2f448bba..2959de3906 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -10,8 +10,8 @@ import { DSpaceObject } from './dspace-object.model'; import { HALLink } from './hal-link.model'; import { License } from './license.model'; import { LICENSE } from './license.resource-type'; -import { ResourcePolicy } from './resource-policy.model'; -import { RESOURCE_POLICY } from './resource-policy.resource-type'; +import { ResourcePolicy } from '../resource-policy/models/resource-policy.model'; +import { RESOURCE_POLICY } from '../resource-policy/models/resource-policy.resource-type'; import { COMMUNITY } from './community.resource-type'; import { Community } from './community.model'; import { ChildHALResource } from './child-hal-resource.model'; diff --git a/src/app/core/shared/resource-policy.model.ts b/src/app/core/shared/resource-policy.model.ts deleted file mode 100644 index dd00a16e97..0000000000 --- a/src/app/core/shared/resource-policy.model.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { autoserialize, deserialize, deserializeAs } from 'cerialize'; -import { typedObject } from '../cache/builders/build-decorators'; -import { IDToUUIDSerializer } from '../cache/id-to-uuid-serializer'; -import { ActionType } from '../cache/models/action-type.model'; -import { CacheableObject } from '../cache/object-cache.reducer'; -import { excludeFromEquals } from '../utilities/equals.decorators'; -import { HALLink } from './hal-link.model'; -import { RESOURCE_POLICY } from './resource-policy.resource-type'; -import { ResourceType } from './resource-type'; - -/** - * Model class for a Resource Policy - */ -@typedObject -export class ResourcePolicy implements CacheableObject { - static type = RESOURCE_POLICY; - - /** - * The object type - */ - @excludeFromEquals - @autoserialize - type: ResourceType; - - /** - * The action that is allowed by this Resource Policy - */ - @autoserialize - action: ActionType; - - /** - * The name for this Resource Policy - */ - @autoserialize - name: string; - - /** - * The uuid of the Group this Resource Policy applies to - */ - @autoserialize - groupUUID: string; - - /** - * The universally unique identifier for this Resource Policy - * This UUID is generated client-side and isn't used by the backend. - * It is based on the ID, so it will be the same for each refresh. - */ - @deserializeAs(new IDToUUIDSerializer('resource-policy'), 'id') - uuid: string; - - /** - * The {@link HALLink}s for this ResourcePolicy - */ - @deserialize - _links: { - self: HALLink, - } -} diff --git a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts index 04852cc014..cb267f70c0 100644 --- a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts +++ b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts @@ -3,7 +3,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { find } from 'rxjs/operators'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; -import { ResourcePolicy } from '../../../../core/shared/resource-policy.model'; +import { ResourcePolicy } from '../../../../core/resource-policy/models/resource-policy.model'; import { isEmpty } from '../../../../shared/empty.util'; import { Group } from '../../../../core/eperson/models/group.model'; import { RemoteData } from '../../../../core/data/remote-data'; diff --git a/src/app/submission/sections/upload/section-upload.component.spec.ts b/src/app/submission/sections/upload/section-upload.component.spec.ts index af865b81eb..02b5b2a67e 100644 --- a/src/app/submission/sections/upload/section-upload.component.spec.ts +++ b/src/app/submission/sections/upload/section-upload.component.spec.ts @@ -19,7 +19,8 @@ import { mockSubmissionId, mockSubmissionState, mockUploadConfigResponse, - mockUploadConfigResponseNotRequired, mockUploadFiles, + mockUploadConfigResponseNotRequired, + mockUploadFiles, } from '../../../shared/mocks/mock-submission'; import { BrowserModule } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; @@ -28,10 +29,10 @@ import { SectionUploadService } from './section-upload.service'; import { SubmissionSectionUploadComponent } from './section-upload.component'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; -import { cold, hot } from 'jasmine-marbles'; +import { cold } from 'jasmine-marbles'; import { Collection } from '../../../core/shared/collection.model'; -import { ResourcePolicy } from '../../../core/shared/resource-policy.model'; -import { ResourcePolicyService } from '../../../core/data/resource-policy.service'; +import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; +import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; import { ConfigData } from '../../../core/config/config-data'; import { PageInfo } from '../../../core/shared/page-info.model'; import { Group } from '../../../core/eperson/models/group.model'; diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index f8f096d4bd..0be249371c 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -1,15 +1,14 @@ import { ChangeDetectorRef, Component, Inject } from '@angular/core'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription} from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { distinctUntilChanged, filter, find, flatMap, map, reduce, take, tap } from 'rxjs/operators'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { SectionModelComponent } from '../models/section.model'; import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shared/empty.util'; import { SectionUploadService } from './section-upload.service'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; -import { ResourcePolicyService } from '../../../core/data/resource-policy.service'; +import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service'; import { SubmissionUploadsModel } from '../../../core/config/models/config-submission-uploads.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; @@ -23,7 +22,6 @@ import { Group } from '../../../core/eperson/models/group.model'; import { SectionsService } from '../sections.service'; import { SubmissionService } from '../../submission.service'; import { Collection } from '../../../core/shared/collection.model'; -import { ResourcePolicy } from '../../../core/shared/resource-policy.model'; import { AccessConditionOption } from '../../../core/config/models/config-access-condition-option.model'; import { PaginatedList } from '../../../core/data/paginated-list'; From ac323f48cca8c43cb8321d74011a23211db26351 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 27 Mar 2020 12:17:59 +0100 Subject: [PATCH 002/103] Added item authorizations component --- resources/i18n/en.json5 | 3 ++ .../edit-item-page/edit-item-page.module.ts | 2 + .../edit-item-page.routing.module.ts | 7 +++ .../item-authorizations.component.html | 3 ++ .../item-authorizations.component.spec.ts | 9 ++++ .../item-authorizations.component.ts | 46 +++++++++++++++++++ .../item-status/item-status.component.ts | 1 + .../resource-policy.service.ts | 6 +-- 8 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html create mode 100644 src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts create mode 100644 src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 9dfcf50a76..8bfc282d73 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -745,6 +745,9 @@ + "item.edit.authorizations.title": "Edit item's Policies", + + "item.edit.delete.cancel": "Cancel", "item.edit.delete.confirm": "Delete", diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index 2cbd0c57d1..2b1248e61f 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -23,6 +23,7 @@ import { EditRelationshipListComponent } from './item-relationships/edit-relatio import { ItemMoveComponent } from './item-move/item-move.component'; import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; +import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -55,6 +56,7 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version ItemCollectionMapperComponent, ItemMoveComponent, VirtualMetadataComponent, + ItemAuthorizationsComponent ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index e4b1b06730..b41df21eaf 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -14,6 +14,7 @@ import { ItemMoveComponent } from './item-move/item-move.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; +import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; export const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; @@ -21,6 +22,7 @@ export const ITEM_EDIT_PRIVATE_PATH = 'private'; export const ITEM_EDIT_PUBLIC_PATH = 'public'; export const ITEM_EDIT_DELETE_PATH = 'delete'; export const ITEM_EDIT_MOVE_PATH = 'move'; +export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations'; /** * Routing module that handles the routing for the Edit Item page administrator functionality @@ -111,6 +113,11 @@ export const ITEM_EDIT_MOVE_PATH = 'move'; path: ITEM_EDIT_MOVE_PATH, component: ItemMoveComponent, data: { title: 'item.edit.move.title' }, + }, + { + path: ITEM_EDIT_AUTHORIZATIONS_PATH, + component: ItemAuthorizationsComponent, + data: { title: 'item.edit.authorizations.title' }, } ] } diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html new file mode 100644 index 0000000000..4ddf939631 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts new file mode 100644 index 0000000000..940c5a0ef5 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts @@ -0,0 +1,9 @@ +import { ComponentFixture } from '@angular/core/testing'; + +import { ItemAuthorizationsComponent } from './item-authorizations.component'; + +describe('ItemAuthorizationsComponent', () => { + let comp: ItemAuthorizationsComponent; + let fixture: ComponentFixture; + +}); diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts new file mode 100644 index 0000000000..21971a09d5 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { flatMap, map } from 'rxjs/operators'; + +import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { LinkService } from '../../../core/cache/builders/link.service'; +import { Bundle } from '../../../core/shared/bundle.model'; + +@Component({ + selector: 'ds-item-authorizations', + templateUrl: './item-authorizations.component.html' +}) +/** + * Component that handles the item Authorizations + */ +export class ItemAuthorizationsComponent implements OnInit { + + private bundles$: Observable>>; + private item$: Observable; + + constructor( + private linkService: LinkService, + private resourcePolicyService: ResourcePolicyService, + private route: ActivatedRoute + ) { + } + + ngOnInit(): void { + this.item$ = this.route.data.pipe( + map((data) => data.item), + getFirstSucceededRemoteDataPayload(), + map((item: Item) => this.linkService.resolveLink(item, followLink('bundles'))) + ) as Observable; + + this.bundles$ = this.item$.pipe(flatMap((item: Item) => item.bundles)); + + } + +} diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index e63154918b..1be13e3a7a 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -68,6 +68,7 @@ export class ItemStatusComponent implements OnInit { The value is supposed to be a href for the button */ this.operations = []; + this.operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations')); this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper')); if (item.isWithdrawn) { this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate')); diff --git a/src/app/core/resource-policy/resource-policy.service.ts b/src/app/core/resource-policy/resource-policy.service.ts index e79f04eb6f..b9dd131fbe 100644 --- a/src/app/core/resource-policy/resource-policy.service.ts +++ b/src/app/core/resource-policy/resource-policy.service.ts @@ -106,7 +106,7 @@ export class ResourcePolicyService { * @param resourceUUID Limit the returned policies to the specified DSO * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - searchByEPerson(UUID: string, resourceUUID?: string, ...linksToFollow: Array>) { + searchByEPerson(UUID: string, resourceUUID?: string, ...linksToFollow: Array>): Observable>> { const options = new FindListOptions(); options.searchParams = [new SearchParam('uuid', UUID)]; if (isNotEmpty(resourceUUID)) { @@ -122,7 +122,7 @@ export class ResourcePolicyService { * @param resourceUUID Limit the returned policies to the specified DSO * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - searchByGroup(UUID: string, resourceUUID?: string, ...linksToFollow: Array>) { + searchByGroup(UUID: string, resourceUUID?: string, ...linksToFollow: Array>): Observable>> { const options = new FindListOptions(); options.searchParams = [new SearchParam('uuid', UUID)]; if (isNotEmpty(resourceUUID)) { @@ -138,7 +138,7 @@ export class ResourcePolicyService { * @param action Limit the returned policies to the specified {@link ActionType} * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - searchByResource(UUID: string, action?: ActionType, ...linksToFollow: Array>) { + searchByResource(UUID: string, action?: ActionType, ...linksToFollow: Array>): Observable>> { const options = new FindListOptions(); options.searchParams = [new SearchParam('uuid', UUID)]; if (isNotEmpty(action)) { From e87c173e96af1561c92d67af194127b533c61853 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 27 Mar 2020 12:19:20 +0100 Subject: [PATCH 003/103] Fixed issue with getSearchEndpoint which make wrong request to endpointMap --- src/app/core/data/data.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 135834b430..d3bca871be 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -292,9 +292,9 @@ export abstract class DataService { * @param searchMethod The search method for the object */ protected getSearchEndpoint(searchMethod: string): Observable { - return this.halService.getEndpoint(`${this.linkPath}/search`).pipe( + return this.halService.getEndpoint(this.linkPath).pipe( filter((href: string) => isNotEmpty(href)), - map((href: string) => `${href}/${searchMethod}`)); + map((href: string) => `${href}/search/${searchMethod}`)); } /** From e48b8ad14714767ea88075a4b9a6e089a3da97a7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 27 Mar 2020 12:21:53 +0100 Subject: [PATCH 004/103] Fixed issue with accessCondition in SubmissionSectionUploadAccessConditionsComponent --- .../submission-section-upload-access-conditions.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts index cb267f70c0..07318807b6 100644 --- a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts +++ b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts @@ -42,7 +42,7 @@ export class SubmissionSectionUploadAccessConditionsComponent implements OnInit ngOnInit() { this.accessConditions.forEach((accessCondition: ResourcePolicy) => { if (isEmpty(accessCondition.name)) { - this.groupService.findById(accessCondition.groupUUID).pipe( + this.groupService.findByHref(accessCondition._links.group.href).pipe( find((rd: RemoteData) => !rd.isResponsePending && rd.hasSucceeded)) .subscribe((rd: RemoteData) => { const group: Group = rd.payload; From 1af5958fa9ea336ee8433a712facc341f0766545 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 30 Mar 2020 11:40:58 +0200 Subject: [PATCH 005/103] Added ngFor track by DSO's id directive --- src/app/shared/ng-for-track-by-id.directive.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/app/shared/ng-for-track-by-id.directive.ts diff --git a/src/app/shared/ng-for-track-by-id.directive.ts b/src/app/shared/ng-for-track-by-id.directive.ts new file mode 100644 index 0000000000..22343df750 --- /dev/null +++ b/src/app/shared/ng-for-track-by-id.directive.ts @@ -0,0 +1,16 @@ +import { Directive, Host } from '@angular/core'; +import { NgForOf } from '@angular/common'; + +import { DSpaceObject } from '../core/shared/dspace-object.model'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[ngForTrackById]', +}) +export class NgForTrackByIdDirective { + + constructor(@Host() private ngFor: NgForOf) { + this.ngFor.ngForTrackBy = (index: number, dso: T) => (dso) ? dso.id : undefined; + } + +} From 2e94b349f9fe7eb30d12707e83708dc46559422d Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 30 Mar 2020 11:43:02 +0200 Subject: [PATCH 006/103] Added eperson and group property to ResourcePolicy model --- .../models/resource-policy.model.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/app/core/resource-policy/models/resource-policy.model.ts b/src/app/core/resource-policy/models/resource-policy.model.ts index cf040867b0..27602557d6 100644 --- a/src/app/core/resource-policy/models/resource-policy.model.ts +++ b/src/app/core/resource-policy/models/resource-policy.model.ts @@ -1,5 +1,5 @@ import { autoserialize, deserialize, deserializeAs } from 'cerialize'; -import { typedObject } from '../../cache/builders/build-decorators'; +import { link, typedObject } from '../../cache/builders/build-decorators'; import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; import { ActionType } from './action-type.model'; import { CacheableObject } from '../../cache/object-cache.reducer'; @@ -8,6 +8,12 @@ import { RESOURCE_POLICY } from './resource-policy.resource-type'; import { excludeFromEquals } from '../../utilities/equals.decorators'; import { ResourceType } from '../../shared/resource-type'; import { PolicyType } from './policy-type.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../data/remote-data'; +import { GROUP } from '../../eperson/models/group.resource-type'; +import { Group } from '../../eperson/models/group.model'; +import { EPERSON } from '../../eperson/models/eperson.resource-type'; +import { EPerson } from '../../eperson/models/eperson.model'; /** * Model class for a Resource Policy @@ -81,5 +87,19 @@ export class ResourcePolicy implements CacheableObject { eperson: HALLink, group: HALLink, self: HALLink, - } + }; + + /** + * The eperson linked by this resource policy + * Will be undefined unless the version {@link HALLink} has been resolved. + */ + @link(EPERSON) + eperson?: Observable>; + + /** + * The group linked by this resource policy + * Will be undefined unless the version {@link HALLink} has been resolved. + */ + @link(GROUP) + group?: Observable>; } From be8923d572cf54b8ea01f3dfe92ddc42b4bd33c2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 30 Mar 2020 11:43:38 +0200 Subject: [PATCH 007/103] Added primaryBitstream and bitstreams properties to Bundle model --- src/app/core/shared/bundle.model.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index c1164f0fc4..1e5c14d486 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,8 +1,15 @@ import { deserialize, inheritSerialization } from 'cerialize'; -import { typedObject } from '../cache/builders/build-decorators'; + +import { Observable } from 'rxjs'; + +import { link, typedObject } from '../cache/builders/build-decorators'; import { BUNDLE } from './bundle.resource-type'; import { DSpaceObject } from './dspace-object.model'; import { HALLink } from './hal-link.model'; +import { RemoteData } from '../data/remote-data'; +import { PaginatedList } from '../data/paginated-list'; +import { BITSTREAM } from './bitstream.resource-type'; +import { Bitstream } from './bitstream.model'; @typedObject @inheritSerialization(DSpaceObject) @@ -17,5 +24,19 @@ export class Bundle extends DSpaceObject { self: HALLink; primaryBitstream: HALLink; bitstreams: HALLink; - } + }; + + /** + * The primary Bitstream of this Bundle + * Will be undefined unless the primaryBitstream {@link HALLink} has been resolved. + */ + @link(BITSTREAM) + primaryBitstream?: Observable>; + + /** + * The list of Bitstreams that are direct children of this Bundle + * Will be undefined unless the bitstreams {@link HALLink} has been resolved. + */ + @link(BITSTREAM, true) + bitstreams?: Observable>>; } From 18d38ca7376d01bea2a22de8048f07ff0424a380 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 30 Mar 2020 12:36:36 +0200 Subject: [PATCH 008/103] Implemented resource policies component --- resources/i18n/en.json5 | 30 ++++ .../resource-policies.component.html | 40 ++++++ .../resource-policies.component.scss | 3 + .../resource-policies.component.ts | 134 ++++++++++++++++++ src/app/shared/shared.module.ts | 6 +- 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/resource-policies/resource-policies.component.html create mode 100644 src/app/shared/resource-policies/resource-policies.component.scss create mode 100644 src/app/shared/resource-policies/resource-policies.component.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 8bfc282d73..b350d2c979 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1657,6 +1657,36 @@ + "resource-policies.add.for.": "Add a new policy", + + "resource-policies.add.for.bitstream": "Add a new Bitstream policy", + + "resource-policies.add.for.bundle": "Add a new Bundle policy", + + "resource-policies.add.for.item": "Add a new Item policy", + + "resource-policies.table.headers.action": "Action", + + "resource-policies.table.headers.date.end": "End Date", + + "resource-policies.table.headers.date.start": "Start Date", + + "resource-policies.table.headers.group": "Group", + + "resource-policies.table.headers.group.edit": "Edit", + + "resource-policies.table.headers.name": "Name", + + "resource-policies.table.headers.id": "ID", + + "resource-policies.table.headers.title.for.bitstream": "Policies for Bitstream", + + "resource-policies.table.headers.title.for.bundle": "Policies for Bundle", + + "resource-policies.table.headers.title.for.item": "Policies for Item", + + + "search.description": "", "search.switch-configuration.title": "Show", diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html new file mode 100644 index 0000000000..dbcf3a45e7 --- /dev/null +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -0,0 +1,40 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ {{ 'resource-policies.table.headers.title.for.' + resourceKey | translate }} {{resourceUUID}} + +

+
{{'resource-policies.table.headers.id' | translate}}{{'resource-policies.table.headers.name' | translate}}{{'resource-policies.table.headers.action' | translate}}{{'resource-policies.table.headers.group' | translate}}{{'resource-policies.table.headers.date.start' | translate}}{{'resource-policies.table.headers.date.end' | translate}}
{{policy.id}}{{policy.name}}{{policy.action}} + {{getGroupName(policy) | async}} + + {{policy.startDate}}{{policy.endDate}}
+
diff --git a/src/app/shared/resource-policies/resource-policies.component.scss b/src/app/shared/resource-policies/resource-policies.component.scss new file mode 100644 index 0000000000..0d9329e760 --- /dev/null +++ b/src/app/shared/resource-policies/resource-policies.component.scss @@ -0,0 +1,3 @@ +td .btn-link:focus { + box-shadow: none !important; +} diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts new file mode 100644 index 0000000000..0596dba586 --- /dev/null +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -0,0 +1,134 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Observable, Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { getFirstSucceededRemoteDataPayload, getSucceededRemoteData } from '../../core/shared/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { ResourcePolicy } from '../../core/resource-policy/models/resource-policy.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { Group } from '../../core/eperson/models/group.model'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { hasValue, isNotEmpty } from '../empty.util'; + +@Component({ + selector: 'ds-resource-policies', + styleUrls: ['./resource-policies.component.scss'], + templateUrl: './resource-policies.component.html' +}) +/** + * Component that shows the policies for given resource + */ +export class ResourcePoliciesComponent implements OnInit, OnDestroy { + + /** + * The resource UUID + * @type {string} + */ + @Input() public resourceUUID: string; + + /** + * The resource type (e.g. 'item', 'bundle' etc) used as key to build automatically translation label + * @type {string} + */ + @Input() public resourceKey: string; + + /** + * The list of policies for given resource + * @type {Observable>>} + */ + private resourcePolicies$: Observable>>; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + private subs: Subscription[] = []; + + /** + * Initialize instance variables + * + * @param {DSONameService} dsoNameService + * @param {GroupDataService} groupService + * @param {ResourcePolicyService} resourcePolicyService + * @param {Router} router + */ + constructor( + private dsoNameService: DSONameService, + private groupService: GroupDataService, + private resourcePolicyService: ResourcePolicyService, + private router: Router + ) { + } + + /** + * Initialize the component, setting up the resource's policies + */ + ngOnInit(): void { + this.resourcePolicies$ = this.resourcePolicyService.searchByResource(this.resourceUUID).pipe( + getSucceededRemoteData() + ); + + } + + /** + * Return the group's name which the given policy is linked to + * + * @param policy The resource policy + */ + getGroupName(policy: ResourcePolicy): Observable { + return this.groupService.findByHref(policy._links.group.href).pipe( + getFirstSucceededRemoteDataPayload(), + // A group has not dc.title metadata so is not possible to use DSONameService to retrieve name + map((group: Group) => group.name) + ) + } + + /** + * Return all resource's policies + * + * @return an observable that emits all resource's policies + */ + getResourcePolicies(): Observable>> { + return this.resourcePolicies$; + } + + /** + * Check whether the given policy is linked to a group + * + * @param policy The resource policy + * @return an observable that emits true when the policy is linked to a group, false otherwise + */ + hasGroup(policy): Observable { + return this.groupService.findByHref(policy._links.group.href).pipe( + getFirstSucceededRemoteDataPayload(), + map((group: Group) => isNotEmpty(group)) + ) + } + + /** + * Redirect to group edit page + * + * @param policy The resource policy + */ + redirectToGroupEditPage(policy: ResourcePolicy): void { + this.subs.push( + this.groupService.findByHref(policy._links.group.href).pipe( + getFirstSucceededRemoteDataPayload(), + map((group: Group) => group.id) + ).subscribe((groupUUID) => this.router.navigate(['groups', groupUUID, 'edit'])) + ) + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()) + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 673c969506..313c56089b 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -179,6 +179,8 @@ import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic- import { ItemVersionsComponent } from './item/item-versions/item-versions.component'; import { SortablejsModule } from 'ngx-sortablejs'; import { MissingTranslationHelper } from './translate/missing-translation.helper'; +import { ResourcePoliciesComponent } from './resource-policies/resource-policies.component'; +import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -347,6 +349,7 @@ const COMPONENTS = [ ExistingMetadataListElementComponent, ItemVersionsComponent, PublicationSearchResultListElementComponent, + ResourcePoliciesComponent ]; const ENTRY_COMPONENTS = [ @@ -438,7 +441,8 @@ const DIRECTIVES = [ AutoFocusDirective, RoleDirective, MetadataRepresentationDirective, - ListableObjectDirective + ListableObjectDirective, + NgForTrackByIdDirective ]; @NgModule({ From 1f26a7b6341e4abf6d3e3fa57cb6377bbd24b9b9 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 30 Mar 2020 12:57:33 +0200 Subject: [PATCH 009/103] Show the policies for each bundle and bitstream within the item --- resources/i18n/en.json5 | 2 + .../item-authorizations.component.html | 12 +- .../item-authorizations.component.ts | 106 ++++++++++++++++-- 3 files changed, 111 insertions(+), 9 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index b350d2c979..e85c374779 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -745,6 +745,8 @@ + "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", + "item.edit.authorizations.title": "Edit item's Policies", diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html index 4ddf939631..cb22d93868 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html @@ -1,3 +1,13 @@
- + + + + + + + +
+ diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index 21971a09d5..e241166a47 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -1,17 +1,24 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Observable } from 'rxjs'; -import { flatMap, map } from 'rxjs/operators'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { catchError, filter, first, flatMap, map, take } from 'rxjs/operators'; import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; import { PaginatedList } from '../../../core/data/paginated-list'; import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; -import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { LinkService } from '../../../core/cache/builders/link.service'; import { Bundle } from '../../../core/shared/bundle.model'; +import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { FindListOptions } from '../../../core/data/request.models'; + +interface BundleBitstreamsMapEntry { + id: string; + bitstreams: Observable> +} @Component({ selector: 'ds-item-authorizations', @@ -20,11 +27,35 @@ import { Bundle } from '../../../core/shared/bundle.model'; /** * Component that handles the item Authorizations */ -export class ItemAuthorizationsComponent implements OnInit { +export class ItemAuthorizationsComponent implements OnInit, OnDestroy { - private bundles$: Observable>>; + public bundleBitstreamsMap: Map>> = new Map>>(); + + /** + * The list of bundle for the item + * @type {Observable>} + */ + private bundles$: Observable>; + + /** + * The target editing item + * @type {Observable} + */ private item$: Observable; + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + private subs: Subscription[] = []; + + /** + * Initialize instance variables + * + * @param {LinkService} linkService + * @param {ResourcePolicyService} resourcePolicyService + * @param {ActivatedRoute} route + */ constructor( private linkService: LinkService, private resourcePolicyService: ResourcePolicyService, @@ -32,15 +63,74 @@ export class ItemAuthorizationsComponent implements OnInit { ) { } + /** + * Initialize the component, setting up the bundle and bitstream within the item + */ ngOnInit(): void { this.item$ = this.route.data.pipe( map((data) => data.item), getFirstSucceededRemoteDataPayload(), - map((item: Item) => this.linkService.resolveLink(item, followLink('bundles'))) + map((item: Item) => this.linkService.resolveLink( + item, + followLink('bundles', new FindListOptions(), true, followLink('bitstreams')) + )) ) as Observable; - this.bundles$ = this.item$.pipe(flatMap((item: Item) => item.bundles)); + this.bundles$ = this.item$.pipe( + filter((item: Item) => isNotEmpty(item.bundles)), + flatMap((item: Item) => item.bundles), + getFirstSucceededRemoteDataPayload(), + catchError(() => observableOf(new PaginatedList(null, []))) + ); + this.subs.push( + this.bundles$.pipe( + take(1), + flatMap((list: PaginatedList) => list.page), + map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) })) + ).subscribe((entry: BundleBitstreamsMapEntry) => { + this.bundleBitstreamsMap.set(entry.id, entry.bitstreams) + }) + ) } + /** + * Return the item's UUID + */ + getItemUUID(): Observable { + return this.item$.pipe( + map((item: Item) => item.id), + first((UUID: string) => isNotEmpty(UUID)) + ) + } + + /** + * Return all item's bundles + * + * @return an observable that emits all item's bundles + */ + getItemBundles(): Observable> { + return this.bundles$ + } + + /** + * Return all bundle's bitstreams + * + * @return an observable that emits all item's bundles + */ + private getBundleBitstreams(bundle: Bundle): Observable> { + return bundle.bitstreams.pipe( + getFirstSucceededRemoteDataPayload(), + catchError(() => observableOf(new PaginatedList(null, []))) + ) + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()) + } } From 6e49ef68592020971d162fb2f8ac488bd08458ee Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 1 Apr 2020 17:04:54 +0200 Subject: [PATCH 010/103] start of admin workflow ui --- src/app/+admin/admin-routing.module.ts | 14 +- .../admin-sidebar/admin-sidebar.component.ts | 13 ++ .../admin-workflow-page.component.html | 3 + .../admin-workflow-page.component.scss | 0 .../admin-workflow-page.component.spec.ts | 27 ++++ .../admin-workflow-page.component.ts | 18 +++ ...-search-result-grid-element.component.html | 15 ++ ...-search-result-grid-element.component.scss | 0 ...arch-result-grid-element.component.spec.ts | 121 +++++++++++++++ ...ow-search-result-grid-element.component.ts | 75 +++++++++ ...-search-result-list-element.component.html | 12 ++ ...-search-result-list-element.component.scss | 0 ...arch-result-list-element.component.spec.ts | 101 ++++++++++++ ...ow-search-result-list-element.component.ts | 20 +++ ...kflow-search-result-actions.component.html | 7 + ...kflow-search-result-actions.component.scss | 1 + ...ow-search-result-actions.component.spec.ts | 144 ++++++++++++++++++ ...orkflow-search-result-actions.component.ts | 46 ++++++ src/app/+admin/admin.module.ts | 17 ++- .../+login-page/login-page-routing.module.ts | 5 + .../search-page-routing.module.ts | 2 + src/app/+search-page/search-page.module.ts | 4 - src/app/app-routing.module.ts | 2 +- src/app/core/shared/context.model.ts | 1 + 24 files changed, 639 insertions(+), 9 deletions(-) create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-page.component.html create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-page.component.scss create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-page.component.spec.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.html create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.scss create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.spec.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.html create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.scss create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.spec.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.html create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.scss create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.spec.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.ts diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index b4a68d692a..c3732c1ba7 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -4,6 +4,8 @@ import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getAdminModulePath } from '../app-routing.module'; import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; +import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; const REGISTRIES_MODULE_PATH = 'registries'; const ACCESS_CONTROL_MODULE_PATH = 'access-control'; @@ -26,10 +28,18 @@ export function getRegistriesModulePath() { { path: 'search', resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: AdminSearchPageComponent, - data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } + component: AdminWorkflowPageComponent, + data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' } }, + { + path: 'workflow', + component: AdminSearchPageComponent, + } ]) + ], + providers: [ + I18nBreadcrumbResolver, + I18nBreadcrumbsService ] }) export class AdminRoutingModule { diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index e3f55b8e18..e86287f85d 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -439,6 +439,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { icon: 'cogs', index: 9 }, + /* Workflow */ + { + id: 'workflow', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.workflow', + link: '/admin/worklow' + } as LinkMenuItemModel, + icon: 'user-check', + index: 10 + }, ]; menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection)); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html new file mode 100644 index 0000000000..47906405d9 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html @@ -0,0 +1,3 @@ +BLABLABALAB + + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.spec.ts new file mode 100644 index 0000000000..d329497473 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminWorkflowPageComponent } from './admin-workflow-page.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('AdminSearchPageComponent', () => { + let component: AdminWorkflowPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AdminWorkflowPageComponent ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminWorkflowPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts new file mode 100644 index 0000000000..37ddc70692 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts @@ -0,0 +1,18 @@ +import { Component, OnInit } from '@angular/core'; +import { Context } from '../../core/shared/context.model'; + +@Component({ + selector: 'ds-admin-workflow-page', + templateUrl: './admin-workflow-page.component.html', + styleUrls: ['./admin-workflow-page.component.scss'] +}) + +/** + * Component that represents a search page for administrators + */ +export class AdminWorkflowPageComponent { + /** + * The context of this page + */ + context: Context = Context.AdminWorkflowSearch; +} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.html new file mode 100644 index 0000000000..1b8daaaff0 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.html @@ -0,0 +1,15 @@ + + +
+
+ {{ "admin.search.item.private" | translate }} +
+
+ {{ "admin.search.item.withdrawn" | translate }} +
+
+
    +
  • + +
  • +
diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.spec.ts new file mode 100644 index 0000000000..7036e5c5ac --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.spec.ts @@ -0,0 +1,121 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { Bitstream } from '../../../../../core/shared/bitstream.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; +import { SharedModule } from '../../../../../shared/shared.module'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { ItemAdminWorkflowSearchResultGridElementComponent } from './item-admin-workflow-search-result-grid-element.component'; + +describe('ItemAdminSearchResultGridElementComponent', () => { + let component: ItemAdminWorkflowSearchResultGridElementComponent; + let fixture: ComponentFixture; + let id; + let searchResult; + + const mockBitstreamDataService = { + getThumbnailFor(item: Item): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + } + }; + + function init() { + id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + searchResult = new ItemSearchResult(); + searchResult.indexableObject = new Item(); + searchResult.indexableObject.uuid = id; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule( + { + declarations: [ItemAdminWorkflowSearchResultGridElementComponent], + imports: [ + NoopAnimationsModule, + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]), + SharedModule + ], + providers: [ + { provide: TruncatableService, useValue: mockTruncatableService }, + { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemAdminWorkflowSearchResultGridElementComponent); + component = fixture.componentInstance; + component.object = searchResult; + component.linkTypes = CollectionElementLinkType; + component.index = 0; + component.viewModes = ViewMode; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('when the item is not withdrawn', () => { + beforeEach(() => { + component.dso.isWithdrawn = false; + fixture.detectChanges(); + }); + + it('should not show the withdrawn badge', () => { + const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); + expect(badge).toBeNull(); + }); + }); + + describe('when the item is withdrawn', () => { + beforeEach(() => { + component.dso.isWithdrawn = true; + fixture.detectChanges(); + }); + + it('should show the withdrawn badge', () => { + const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); + expect(badge).not.toBeNull(); + }); + }); + + describe('when the item is not private', () => { + beforeEach(() => { + component.dso.isDiscoverable = true; + fixture.detectChanges(); + }); + it('should not show the private badge', () => { + const badge = fixture.debugElement.query(By.css('div.private-badge')); + expect(badge).toBeNull(); + }); + }); + + describe('when the item is private', () => { + beforeEach(() => { + component.dso.isDiscoverable = false; + fixture.detectChanges(); + }); + + it('should show the private badge', () => { + const badge = fixture.debugElement.query(By.css('div.private-badge')); + expect(badge).not.toBeNull(); + }); + }) +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.ts new file mode 100644 index 0000000000..9b3ed12a24 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.ts @@ -0,0 +1,75 @@ +import { Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { Item } from '../../../../../core/shared/item.model'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../../core/shared/context.model'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { getItemEditPath } from '../../../../../+item-page/item-page-routing.module'; +import { URLCombiner } from '../../../../../core/url-combiner/url-combiner'; +import { + ITEM_EDIT_DELETE_PATH, + ITEM_EDIT_MOVE_PATH, + ITEM_EDIT_PRIVATE_PATH, + ITEM_EDIT_PUBLIC_PATH, + ITEM_EDIT_REINSTATE_PATH, + ITEM_EDIT_WITHDRAW_PATH +} from '../../../../../+item-page/edit-item-page/edit-item-page.routing.module'; +import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; +import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; + +@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) +@Component({ + selector: 'ds-item-admin-workflow-search-result-grid-element', + styleUrls: ['./item-admin-workflow-search-result-grid-element.component.scss'], + templateUrl: './item-admin-workflow-search-result-grid-element.component.html' +}) +/** + * The component for displaying a list element for an item search result on the admin search page + */ +export class ItemAdminWorkflowSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnInit { + @ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective; + @ViewChild('badges', { static: true }) badges: ElementRef; + @ViewChild('buttons', { static: true }) buttons: ElementRef; + + constructor(protected truncatableService: TruncatableService, + protected bitstreamDataService: BitstreamDataService, + private componentFactoryResolver: ComponentFactoryResolver + ) { + super(truncatableService, bitstreamDataService); + } + + /** + * Setup the dynamic child component + */ + ngOnInit(): void { + super.ngOnInit(); + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent()); + + const viewContainerRef = this.listableObjectDirective.viewContainerRef; + viewContainerRef.clear(); + + const componentRef = viewContainerRef.createComponent( + componentFactory, + 0, + undefined, + [ + [this.badges.nativeElement], + [this.buttons.nativeElement] + ]); + (componentRef.instance as any).object = this.object; + (componentRef.instance as any).index = this.index; + (componentRef.instance as any).linkType = this.linkType; + (componentRef.instance as any).listID = this.listID; + } + + /** + * Fetch the component depending on the item's relationship type, view mode and context + * @returns {GenericConstructor} + */ + private getComponent(): GenericConstructor { + return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined) + } +} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.html new file mode 100644 index 0000000000..14e6c1bcd1 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.html @@ -0,0 +1,12 @@ +
+ {{ "admin.search.item.private" | translate }} +
+
+ {{ "admin.search.item.withdrawn" | translate }} +
+ + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.spec.ts new file mode 100644 index 0000000000..595a36a5b3 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.spec.ts @@ -0,0 +1,101 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { ItemAdminWorkflowSearchResultListElementComponent } from './item-admin-workflow-search-result-list-element.component'; +import { Item } from '../../../../../core/shared/item.model'; + +describe('ItemAdminSearchResultListElementComponent', () => { + let component: ItemAdminWorkflowSearchResultListElementComponent; + let fixture: ComponentFixture; + let id; + let searchResult; + + function init() { + id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + searchResult = new ItemSearchResult(); + searchResult.indexableObject = new Item(); + searchResult.indexableObject.uuid = id; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]) + ], + declarations: [ItemAdminWorkflowSearchResultListElementComponent], + providers: [{ provide: TruncatableService, useValue: {} }], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemAdminWorkflowSearchResultListElementComponent); + component = fixture.componentInstance; + component.object = searchResult; + component.linkTypes = CollectionElementLinkType; + component.index = 0; + component.viewModes = ViewMode; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('when the item is not withdrawn', () => { + beforeEach(() => { + component.dso.isWithdrawn = false; + fixture.detectChanges(); + }); + + it('should not show the withdrawn badge', () => { + const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); + expect(badge).toBeNull(); + }); + }); + + describe('when the item is withdrawn', () => { + beforeEach(() => { + component.dso.isWithdrawn = true; + fixture.detectChanges(); + }); + + it('should show the withdrawn badge', () => { + const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); + expect(badge).not.toBeNull(); + }); + }); + + describe('when the item is not private', () => { + beforeEach(() => { + component.dso.isDiscoverable = true; + fixture.detectChanges(); + }); + it('should not show the private badge', () => { + const badge = fixture.debugElement.query(By.css('div.private-badge')); + expect(badge).toBeNull(); + }); + }); + + describe('when the item is private', () => { + beforeEach(() => { + component.dso.isDiscoverable = false; + fixture.detectChanges(); + }); + + it('should show the private badge', () => { + const badge = fixture.debugElement.query(By.css('div.private-badge')); + expect(badge).not.toBeNull(); + }); + }) +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.ts new file mode 100644 index 0000000000..9550028919 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { Item } from '../../../../../core/shared/item.model'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../../core/shared/context.model'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; + +@listableObjectComponent(ItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) +@Component({ + selector: 'ds-item-admin-workflow-search-result-list-element', + styleUrls: ['./item-admin-workflow-search-result-list-element.component.scss'], + templateUrl: './item-admin-workflow-search-result-list-element.component.html' +}) +/** + * The component for displaying a list element for an item search result on the admin search page + */ +export class ItemAdminWorkflowSearchResultListElementComponent extends SearchResultListElementComponent { + +} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.html new file mode 100644 index 0000000000..3f80b127a8 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.html @@ -0,0 +1,7 @@ + + {{"admin.search.item.delete" | translate}} + + + + {{"admin.search.item.send-back" | translate}} + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.spec.ts new file mode 100644 index 0000000000..c1d42341bc --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.spec.ts @@ -0,0 +1,144 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ItemAdminWorkflowSearchResultActionsComponent } from './item-admin-workflow-search-result-actions.component'; +import { Item } from '../../../core/shared/item.model'; +import { + ITEM_EDIT_DELETE_PATH, + ITEM_EDIT_MOVE_PATH, + ITEM_EDIT_PRIVATE_PATH, + ITEM_EDIT_PUBLIC_PATH, + ITEM_EDIT_REINSTATE_PATH, + ITEM_EDIT_WITHDRAW_PATH +} from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; +import { getItemEditPath } from '../../../+item-page/item-page-routing.module'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; + +describe('ItemAdminSearchResultActionsComponent', () => { + let component: ItemAdminWorkflowSearchResultActionsComponent; + let fixture: ComponentFixture; + let id; + let item; + + function init() { + id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + item = new Item(); + item.uuid = id; + } + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]) + ], + declarations: [ItemAdminWorkflowSearchResultActionsComponent], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemAdminWorkflowSearchResultActionsComponent); + component = fixture.componentInstance; + component.item = item; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render an edit button with the correct link', () => { + const button = fixture.debugElement.query(By.css('a.edit-link')); + const link = button.nativeElement.href; + expect(link).toContain(getItemEditPath(id)); + }); + + it('should render a delete button with the correct link', () => { + const button = fixture.debugElement.query(By.css('a.delete-link')); + const link = button.nativeElement.href; + expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_DELETE_PATH).toString()); + }); + + it('should render a move button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.move-link')); + const link = a.nativeElement.href; + expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_MOVE_PATH).toString()); + }); + + describe('when the item is not withdrawn', () => { + beforeEach(() => { + component.item.isWithdrawn = false; + fixture.detectChanges(); + }); + + it('should render a withdraw button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.withdraw-link')); + const link = a.nativeElement.href; + expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_WITHDRAW_PATH).toString()); + }); + + it('should not render a reinstate button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.reinstate-link')); + expect(a).toBeNull(); + }); + }); + + describe('when the item is withdrawn', () => { + beforeEach(() => { + component.item.isWithdrawn = true; + fixture.detectChanges(); + }); + + it('should not render a withdraw button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.withdraw-link')); + expect(a).toBeNull(); + }); + + it('should render a reinstate button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.reinstate-link')); + const link = a.nativeElement.href; + expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_REINSTATE_PATH).toString()); + }); + }); + + describe('when the item is not private', () => { + beforeEach(() => { + component.item.isDiscoverable = true; + fixture.detectChanges(); + }); + + it('should render a make private button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.private-link')); + const link = a.nativeElement.href; + expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PRIVATE_PATH).toString()); + }); + + it('should not render a make public button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.public-link')); + expect(a).toBeNull(); + }); + }); + + describe('when the item is private', () => { + beforeEach(() => { + component.item.isDiscoverable = false; + fixture.detectChanges(); + }); + + it('should not render a make private button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.private-link')); + expect(a).toBeNull(); + }); + + it('should render a make private button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.public-link')); + const link = a.nativeElement.href; + expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PUBLIC_PATH).toString()); + }); + }) +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.ts new file mode 100644 index 0000000000..3dc7b96391 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.ts @@ -0,0 +1,46 @@ +import { Component, Input } from '@angular/core'; +import { Item } from '../../../core/shared/item.model'; +import { getItemEditPath } from '../../../+item-page/item-page-routing.module'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { + ITEM_EDIT_DELETE_PATH, + ITEM_EDIT_MOVE_PATH, + ITEM_EDIT_PRIVATE_PATH, + ITEM_EDIT_PUBLIC_PATH, + ITEM_EDIT_REINSTATE_PATH, + ITEM_EDIT_WITHDRAW_PATH +} from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; + +@Component({ + selector: 'ds-item-admin-workflow-search-result-actions-element', + styleUrls: ['./item-admin-workflow-search-result-actions.component.scss'], + templateUrl: './item-admin-workflow-search-result-actions.component.html' +}) +/** + * The component for displaying the actions for a list element for an item search result on the admin search page + */ +export class ItemAdminWorkflowSearchResultActionsComponent { + /** + * The item to perform the actions on + */ + @Input() public item: Item; + + /** + * Whether or not to use small buttons + */ + @Input() public small: boolean; + + /** + * Returns the path to the edit page of this item + */ + getEditPath(): string { + return getItemEditPath(this.item.uuid) + } + + /** + * Returns the path to the withdraw page of this item + */ + getWithdrawPath(): string { + return new URLCombiner(this.getEditPath(), ITEM_EDIT_WITHDRAW_PATH).toString(); + } +} diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts index fa2480a6ad..d29d876d02 100644 --- a/src/app/+admin/admin.module.ts +++ b/src/app/+admin/admin.module.ts @@ -12,6 +12,10 @@ import { ItemAdminSearchResultGridElementComponent } from './admin-search-page/a import { CommunityAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component'; import { CollectionAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component'; import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin-search-results/item-admin-search-result-actions.component'; +import { ItemAdminWorkflowSearchResultGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component'; +import { ItemAdminWorkflowSearchResultActionsComponent } from './admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component'; +import { ItemAdminWorkflowSearchResultListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component'; +import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; @NgModule({ imports: [ @@ -23,13 +27,18 @@ import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin ], declarations: [ AdminSearchPageComponent, + AdminWorkflowPageComponent, ItemAdminSearchResultListElementComponent, CommunityAdminSearchResultListElementComponent, CollectionAdminSearchResultListElementComponent, ItemAdminSearchResultGridElementComponent, CommunityAdminSearchResultGridElementComponent, CollectionAdminSearchResultGridElementComponent, - ItemAdminSearchResultActionsComponent + ItemAdminSearchResultActionsComponent, + + ItemAdminWorkflowSearchResultListElementComponent, + ItemAdminWorkflowSearchResultGridElementComponent, + ItemAdminWorkflowSearchResultActionsComponent, ], entryComponents: [ ItemAdminSearchResultListElementComponent, @@ -38,7 +47,11 @@ import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin ItemAdminSearchResultGridElementComponent, CommunityAdminSearchResultGridElementComponent, CollectionAdminSearchResultGridElementComponent, - ItemAdminSearchResultActionsComponent + ItemAdminSearchResultActionsComponent, + + ItemAdminWorkflowSearchResultListElementComponent, + ItemAdminWorkflowSearchResultGridElementComponent, + ItemAdminWorkflowSearchResultActionsComponent, ] }) export class AdminModule { diff --git a/src/app/+login-page/login-page-routing.module.ts b/src/app/+login-page/login-page-routing.module.ts index cd023da55c..9fa4a9e5ad 100644 --- a/src/app/+login-page/login-page-routing.module.ts +++ b/src/app/+login-page/login-page-routing.module.ts @@ -3,12 +3,17 @@ import { RouterModule } from '@angular/router'; import { LoginPageComponent } from './login-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; @NgModule({ imports: [ RouterModule.forChild([ { path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } } ]) + ], + providers: [ + I18nBreadcrumbResolver, + I18nBreadcrumbsService ] }) export class LoginPageRoutingModule { diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index 6e36883394..f71c7b45ee 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -6,9 +6,11 @@ import { ConfigurationSearchPageComponent } from './configuration-search-page.co import { SearchPageComponent } from './search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; +import { SearchPageModule } from './search-page.module'; @NgModule({ imports: [ + SearchPageModule, RouterModule.forChild([{ path: '', resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' }, diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index b69dcaf935..00c990c665 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -2,10 +2,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; -import { SearchPageRoutingModule } from './search-page-routing.module'; import { SearchComponent } from './search.component'; import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { EffectsModule } from '@ngrx/effects'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; import { SearchTrackerComponent } from './search-tracker.component'; @@ -14,7 +12,6 @@ import { SearchPageComponent } from './search-page.component'; import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service'; import { SearchFilterService } from '../core/shared/search/search-filter.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { TranslateModule } from '@ngx-translate/core'; const components = [ SearchPageComponent, @@ -25,7 +22,6 @@ const components = [ @NgModule({ imports: [ - SearchPageRoutingModule, CommonModule, SharedModule, CoreModule.forRoot(), diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2927cd4e65..06811abd08 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -68,7 +68,7 @@ export function getDSOPath(dso: DSpaceObject): string { loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] }, - { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' }, + { path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' }, { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'}, { path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, diff --git a/src/app/core/shared/context.model.ts b/src/app/core/shared/context.model.ts index 6bb3d77140..ff24b7d090 100644 --- a/src/app/core/shared/context.model.ts +++ b/src/app/core/shared/context.model.ts @@ -11,4 +11,5 @@ export enum Context { AdminMenu = 'adminMenu', SubmissionModal = 'submissionModal', AdminSearch = 'adminSearch', + AdminWorkflowSearch = 'adminWorkflowSearch', } From 258b1228f317e8392758efb5f0211b6df0a209d2 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 2 Apr 2020 17:05:46 +0200 Subject: [PATCH 011/103] replaced wfi components by task components that render wfi's --- resources/i18n/en.json5 | 14 ++ src/app/+admin/admin-routing.module.ts | 10 +- .../admin-sidebar/admin-sidebar.component.ts | 2 +- .../admin-workflow-page.component.html | 4 +- ...-search-result-grid-element.component.html | 15 --- ...-search-result-grid-element.component.html | 6 + ...search-result-grid-element.component.scss} | 0 ...arch-result-grid-element.component.spec.ts | 121 ++++++++++++++++++ ...ow-search-result-grid-element.component.ts | 45 +++++++ ...admin-workflow-grid-element.component.html | 12 ++ ...dmin-workflow-grid-element.component.scss} | 0 ...n-workflow-grid-element.component.spec.ts} | 10 +- ...-admin-workflow-grid-element.component.ts} | 21 ++- ...-search-result-list-element.component.html | 12 -- ...ow-search-result-list-element.component.ts | 20 --- ...-search-result-list-element.component.html | 6 + ...-search-result-list-element.component.scss | 0 ...arch-result-list-element.component.spec.ts | 101 +++++++++++++++ ...ow-search-result-list-element.component.ts | 29 +++++ ...admin-workflow-list-element.component.html | 9 ++ ...admin-workflow-list-element.component.scss | 0 ...n-workflow-list-element.component.spec.ts} | 10 +- ...m-admin-workflow-list-element.component.ts | 19 +++ ...orkflow-search-result-actions.component.ts | 46 ------- ...tem-admin-workflow-actions.component.html} | 0 ...tem-admin-workflow-actions.component.scss} | 0 ...-admin-workflow-actions.component.spec.ts} | 2 +- ...w-item-admin-workflow-actions.component.ts | 39 ++++++ src/app/+admin/admin.module.ts | 26 ++-- .../workflow-item-delete.component.html | 1 + .../workflow-item-delete.component.scss | 0 .../workflow-item-delete.component.spec.ts | 25 ++++ .../workflow-item-delete.component.ts | 16 +++ .../workflowitems-edit-page.module.ts | 3 +- 34 files changed, 491 insertions(+), 133 deletions(-) delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.html create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.html rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/{item-search-result/item-admin-workflow-search-result-grid-element.component.scss => pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.scss} (100%) create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.spec.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/{admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.scss => admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.scss} (100%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/{item-search-result/item-admin-workflow-search-result-grid-element.component.spec.ts => workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts} (90%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/{item-search-result/item-admin-workflow-search-result-grid-element.component.ts => workflow-item/workflow-item-admin-workflow-grid-element.component.ts} (76%) delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.html delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.scss create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.spec.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.scss rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/{item-search-result/item-admin-workflow-search-result-list-element.component.spec.ts => workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts} (87%) create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.ts rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/{item-admin-workflow-search-result-actions.component.html => workflow-item-admin-workflow-actions.component.html} (100%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/{item-admin-workflow-search-result-actions.component.scss => workflow-item-admin-workflow-actions.component.scss} (100%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/{item-admin-workflow-search-result-actions.component.spec.ts => workflow-item-admin-workflow-actions.component.spec.ts} (98%) create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts create mode 100644 src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html create mode 100644 src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.scss create mode 100644 src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts create mode 100644 src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 7b814fd5bc..f874a02bdf 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -262,6 +262,14 @@ + + "admin.workflow.breadcrumbs": "Admin Workflow", + + "admin.workflow.title": "Admin Workflow", + + + + "auth.errors.invalid-user": "Invalid email address or password.", "auth.messages.expired": "Your session has expired. Please log in again.", @@ -1385,6 +1393,8 @@ "menu.section.toggle.statistics_task": "Toggle Statistics Task section", + "menu.section.workflow": "Active Workflows", + "mydspace.description": "", @@ -2213,5 +2223,9 @@ "virtual-metadata.delete-item.modal-head": "The virtual metadata of this relation", "virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata", + + "workflowAdmin.search.results.head": "Active Workflows", + + "workflowAdmin.search.results.head": "Active Workflows", } diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index c3732c1ba7..5cb10c38ba 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -28,13 +28,15 @@ export function getRegistriesModulePath() { { path: 'search', resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: AdminWorkflowPageComponent, - data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' } + component: AdminSearchPageComponent, + data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } }, { path: 'workflow', - component: AdminSearchPageComponent, - } + resolve: { breadcrumb: I18nBreadcrumbResolver }, + component: AdminWorkflowPageComponent, + data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' } + }, ]) ], providers: [ diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index e86287f85d..e35a63342a 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -447,7 +447,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { model: { type: MenuItemType.LINK, text: 'menu.section.workflow', - link: '/admin/worklow' + link: '/admin/workflow' } as LinkMenuItemModel, icon: 'user-check', index: 10 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html index 47906405d9..404af131d1 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html +++ b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html @@ -1,3 +1 @@ -BLABLABALAB - - + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.html deleted file mode 100644 index 1b8daaaff0..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - -
-
- {{ "admin.search.item.private" | translate }} -
-
- {{ "admin.search.item.withdrawn" | translate }} -
-
-
    -
  • - -
  • -
diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.html new file mode 100644 index 0000000000..c73a5f5a02 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.html @@ -0,0 +1,6 @@ + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.scss similarity index 100% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.scss rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.scss diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.spec.ts new file mode 100644 index 0000000000..33881dfe70 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.spec.ts @@ -0,0 +1,121 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { Bitstream } from '../../../../../core/shared/bitstream.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; +import { SharedModule } from '../../../../../shared/shared.module'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { PoolTaskAdminWorkflowSearchResultGridElementComponent } from './pool-task-admin-workflow-search-result-grid-element.component'; + +describe('ItemAdminSearchResultGridElementComponent', () => { + let component: PoolTaskAdminWorkflowSearchResultGridElementComponent; + let fixture: ComponentFixture; + let id; + let searchResult; + + const mockBitstreamDataService = { + getThumbnailFor(item: Item): Observable> { + return createSuccessfulRemoteDataObject$(new Bitstream()); + } + }; + + function init() { + id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + searchResult = new ItemSearchResult(); + searchResult.indexableObject = new Item(); + searchResult.indexableObject.uuid = id; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule( + { + declarations: [PoolTaskAdminWorkflowSearchResultGridElementComponent], + imports: [ + NoopAnimationsModule, + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]), + SharedModule + ], + providers: [ + { provide: TruncatableService, useValue: mockTruncatableService }, + { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PoolTaskAdminWorkflowSearchResultGridElementComponent); + component = fixture.componentInstance; + component.object = searchResult; + component.linkTypes = CollectionElementLinkType; + component.index = 0; + component.viewModes = ViewMode; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('when the item is not withdrawn', () => { + beforeEach(() => { + component.dso.isWithdrawn = false; + fixture.detectChanges(); + }); + + it('should not show the withdrawn badge', () => { + const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); + expect(badge).toBeNull(); + }); + }); + + describe('when the item is withdrawn', () => { + beforeEach(() => { + component.dso.isWithdrawn = true; + fixture.detectChanges(); + }); + + it('should show the withdrawn badge', () => { + const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); + expect(badge).not.toBeNull(); + }); + }); + + describe('when the item is not private', () => { + beforeEach(() => { + component.dso.isDiscoverable = true; + fixture.detectChanges(); + }); + it('should not show the private badge', () => { + const badge = fixture.debugElement.query(By.css('div.private-badge')); + expect(badge).toBeNull(); + }); + }); + + describe('when the item is private', () => { + beforeEach(() => { + component.dso.isDiscoverable = false; + fixture.detectChanges(); + }); + + it('should show the private badge', () => { + const badge = fixture.debugElement.query(By.css('div.private-badge')); + expect(badge).not.toBeNull(); + }); + }) +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts new file mode 100644 index 0000000000..e386ec45ae --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts @@ -0,0 +1,45 @@ +import { Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { Item } from '../../../../../core/shared/item.model'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../../core/shared/context.model'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { getItemEditPath } from '../../../../../+item-page/item-page-routing.module'; +import { URLCombiner } from '../../../../../core/url-combiner/url-combiner'; +import { + ITEM_EDIT_DELETE_PATH, + ITEM_EDIT_MOVE_PATH, + ITEM_EDIT_PRIVATE_PATH, + ITEM_EDIT_PUBLIC_PATH, + ITEM_EDIT_REINSTATE_PATH, + ITEM_EDIT_WITHDRAW_PATH +} from '../../../../../+item-page/edit-item-page/edit-item-page.routing.module'; +import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; +import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; +import { PoolTaskSearchResult } from '../../../../../shared/object-collection/shared/pool-task-search-result.model'; +import { PoolTask } from '../../../../../core/tasks/models/pool-task-object.model'; +import { Observable } from 'rxjs'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; + +@listableObjectComponent(PoolTaskSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) +@Component({ + selector: 'ds-pool-task-admin-workflow-search-result-grid-element', + styleUrls: ['./pool-task-admin-workflow-search-result-grid-element.component.scss'], + templateUrl: './pool-task-admin-workflow-search-result-grid-element.component.html' +}) +/** + * The component for displaying a list element for an pool task search result on the admin search page + */ +export class PoolTaskAdminWorkflowSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnInit { + public wfi$: Observable; + + ngOnInit(): void { + super.ngOnInit(); + this.wfi$ = (this.dso.workflowitem as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); + } +} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html new file mode 100644 index 0000000000..6f940d097e --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html @@ -0,0 +1,12 @@ + + +
+
+ {{ "admin.workflow.item.workflow" | translate }} +
+
+
    +
  • + +
  • +
diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.scss similarity index 100% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.scss rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.scss diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts similarity index 90% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.spec.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts index 7036e5c5ac..61bcdde6cf 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts @@ -17,11 +17,11 @@ import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; -import { ItemAdminWorkflowSearchResultGridElementComponent } from './item-admin-workflow-search-result-grid-element.component'; +import { WorkflowItemAdminWorkflowGridElementComponent } from './workflow-item-admin-workflow-grid-element.component'; describe('ItemAdminSearchResultGridElementComponent', () => { - let component: ItemAdminWorkflowSearchResultGridElementComponent; - let fixture: ComponentFixture; + let component: WorkflowItemAdminWorkflowGridElementComponent; + let fixture: ComponentFixture; let id; let searchResult; @@ -42,7 +42,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { init(); TestBed.configureTestingModule( { - declarations: [ItemAdminWorkflowSearchResultGridElementComponent], + declarations: [WorkflowItemAdminWorkflowGridElementComponent], imports: [ NoopAnimationsModule, TranslateModule.forRoot(), @@ -59,7 +59,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(ItemAdminWorkflowSearchResultGridElementComponent); + fixture = TestBed.createComponent(WorkflowItemAdminWorkflowGridElementComponent); component = fixture.componentInstance; component.object = searchResult; component.linkTypes = CollectionElementLinkType; diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts similarity index 76% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts index 9b3ed12a24..f00dc4fdb4 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts @@ -19,33 +19,32 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; +import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component'; @listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ - selector: 'ds-item-admin-workflow-search-result-grid-element', - styleUrls: ['./item-admin-workflow-search-result-grid-element.component.scss'], - templateUrl: './item-admin-workflow-search-result-grid-element.component.html' + selector: 'ds-workflow-item-admin-workflow-grid-element', + styleUrls: ['./workflow-item-admin-workflow-grid-element.component.scss'], + templateUrl: './workflow-item-admin-workflow-grid-element.component.html' }) /** - * The component for displaying a list element for an item search result on the admin search page + * The component for displaying a list element for an workflow item on the admin search page */ -export class ItemAdminWorkflowSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnInit { +export class WorkflowItemAdminWorkflowGridElementComponent extends AbstractListableElementComponent { @ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective; @ViewChild('badges', { static: true }) badges: ElementRef; @ViewChild('buttons', { static: true }) buttons: ElementRef; - constructor(protected truncatableService: TruncatableService, - protected bitstreamDataService: BitstreamDataService, - private componentFactoryResolver: ComponentFactoryResolver - ) { - super(truncatableService, bitstreamDataService); + constructor(private componentFactoryResolver: ComponentFactoryResolver) { + super(); } /** * Setup the dynamic child component */ ngOnInit(): void { - super.ngOnInit(); const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent()); const viewContainerRef = this.listableObjectDirective.viewContainerRef; diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.html deleted file mode 100644 index 14e6c1bcd1..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.html +++ /dev/null @@ -1,12 +0,0 @@ -
- {{ "admin.search.item.private" | translate }} -
-
- {{ "admin.search.item.withdrawn" | translate }} -
- - diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.ts deleted file mode 100644 index 9550028919..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component } from '@angular/core'; -import { Item } from '../../../../../core/shared/item.model'; -import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; -import { Context } from '../../../../../core/shared/context.model'; -import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; -import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; - -@listableObjectComponent(ItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) -@Component({ - selector: 'ds-item-admin-workflow-search-result-list-element', - styleUrls: ['./item-admin-workflow-search-result-list-element.component.scss'], - templateUrl: './item-admin-workflow-search-result-list-element.component.html' -}) -/** - * The component for displaying a list element for an item search result on the admin search page - */ -export class ItemAdminWorkflowSearchResultListElementComponent extends SearchResultListElementComponent { - -} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html new file mode 100644 index 0000000000..e894eedb94 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html @@ -0,0 +1,6 @@ + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.spec.ts new file mode 100644 index 0000000000..63f36ea033 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.spec.ts @@ -0,0 +1,101 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { PoolTaskAdminWorkflowSearchResultListElementComponent } from './pool-task-admin-workflow-search-result-list-element.component'; +import { Item } from '../../../../../core/shared/item.model'; + +describe('ItemAdminSearchResultListElementComponent', () => { + let component: PoolTaskAdminWorkflowSearchResultListElementComponent; + let fixture: ComponentFixture; + let id; + let searchResult; + + function init() { + id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + searchResult = new ItemSearchResult(); + searchResult.indexableObject = new Item(); + searchResult.indexableObject.uuid = id; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]) + ], + declarations: [PoolTaskAdminWorkflowSearchResultListElementComponent], + providers: [{ provide: TruncatableService, useValue: {} }], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PoolTaskAdminWorkflowSearchResultListElementComponent); + component = fixture.componentInstance; + component.object = searchResult; + component.linkTypes = CollectionElementLinkType; + component.index = 0; + component.viewModes = ViewMode; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('when the item is not withdrawn', () => { + beforeEach(() => { + component.dso.isWithdrawn = false; + fixture.detectChanges(); + }); + + it('should not show the withdrawn badge', () => { + const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); + expect(badge).toBeNull(); + }); + }); + + describe('when the item is withdrawn', () => { + beforeEach(() => { + component.dso.isWithdrawn = true; + fixture.detectChanges(); + }); + + it('should show the withdrawn badge', () => { + const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); + expect(badge).not.toBeNull(); + }); + }); + + describe('when the item is not private', () => { + beforeEach(() => { + component.dso.isDiscoverable = true; + fixture.detectChanges(); + }); + it('should not show the private badge', () => { + const badge = fixture.debugElement.query(By.css('div.private-badge')); + expect(badge).toBeNull(); + }); + }); + + describe('when the item is private', () => { + beforeEach(() => { + component.dso.isDiscoverable = false; + fixture.detectChanges(); + }); + + it('should show the private badge', () => { + const badge = fixture.debugElement.query(By.css('div.private-badge')); + expect(badge).not.toBeNull(); + }); + }) +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts new file mode 100644 index 0000000000..8a6934e755 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../../core/shared/context.model'; +import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; +import { PoolTaskSearchResult } from '../../../../../shared/object-collection/shared/pool-task-search-result.model'; +import { PoolTask } from '../../../../../core/tasks/models/pool-task-object.model'; +import { Observable, pipe } from 'rxjs'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; + +@listableObjectComponent(PoolTaskSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) +@Component({ + selector: 'ds-pooltask-admin-workflow-search-result-list-element', + styleUrls: ['./pool-task-admin-workflow-search-result-list-element.component.scss'], + templateUrl: './pool-task-admin-workflow-search-result-list-element.component.html' +}) +/** + * The component for displaying a list element for an pool task search result on the admin search page + */ +export class PoolTaskAdminWorkflowSearchResultListElementComponent extends SearchResultListElementComponent { + public wfi$: Observable; + + ngOnInit(): void { + super.ngOnInit(); + this.wfi$ = (this.dso.workflowitem as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); + } +} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html new file mode 100644 index 0000000000..736aba2fce --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html @@ -0,0 +1,9 @@ +
+ {{ "admin.workflow.item.workflow" | translate }} +
+ + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts similarity index 87% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.spec.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts index 595a36a5b3..e406e4c28c 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts @@ -8,12 +8,12 @@ import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; -import { ItemAdminWorkflowSearchResultListElementComponent } from './item-admin-workflow-search-result-list-element.component'; +import { WorkflowItemAdminWorkflowListElementComponent } from './workflow-item-admin-workflow-list-element.component'; import { Item } from '../../../../../core/shared/item.model'; describe('ItemAdminSearchResultListElementComponent', () => { - let component: ItemAdminWorkflowSearchResultListElementComponent; - let fixture: ComponentFixture; + let component: WorkflowItemAdminWorkflowListElementComponent; + let fixture: ComponentFixture; let id; let searchResult; @@ -31,7 +31,7 @@ describe('ItemAdminSearchResultListElementComponent', () => { TranslateModule.forRoot(), RouterTestingModule.withRoutes([]) ], - declarations: [ItemAdminWorkflowSearchResultListElementComponent], + declarations: [WorkflowItemAdminWorkflowListElementComponent], providers: [{ provide: TruncatableService, useValue: {} }], schemas: [NO_ERRORS_SCHEMA] }) @@ -39,7 +39,7 @@ describe('ItemAdminSearchResultListElementComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(ItemAdminWorkflowSearchResultListElementComponent); + fixture = TestBed.createComponent(WorkflowItemAdminWorkflowListElementComponent); component = fixture.componentInstance; component.object = searchResult; component.linkTypes = CollectionElementLinkType; diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts new file mode 100644 index 0000000000..76698507aa --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../../core/shared/context.model'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component'; + +@listableObjectComponent(WorkflowItem, ViewMode.ListElement, Context.AdminWorkflowSearch) +@Component({ + selector: 'ds-workflow-item-admin-workflow-list-element', + styleUrls: ['./workflow-item-admin-workflow-list-element.component.scss'], + templateUrl: './workflow-item-admin-workflow-list-element.component.html' +}) +/** + * The component for displaying a list element for an workflow item on the admin search page + */ +export class WorkflowItemAdminWorkflowListElementComponent extends AbstractListableElementComponent { + +} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.ts deleted file mode 100644 index 3dc7b96391..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { Item } from '../../../core/shared/item.model'; -import { getItemEditPath } from '../../../+item-page/item-page-routing.module'; -import { URLCombiner } from '../../../core/url-combiner/url-combiner'; -import { - ITEM_EDIT_DELETE_PATH, - ITEM_EDIT_MOVE_PATH, - ITEM_EDIT_PRIVATE_PATH, - ITEM_EDIT_PUBLIC_PATH, - ITEM_EDIT_REINSTATE_PATH, - ITEM_EDIT_WITHDRAW_PATH -} from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; - -@Component({ - selector: 'ds-item-admin-workflow-search-result-actions-element', - styleUrls: ['./item-admin-workflow-search-result-actions.component.scss'], - templateUrl: './item-admin-workflow-search-result-actions.component.html' -}) -/** - * The component for displaying the actions for a list element for an item search result on the admin search page - */ -export class ItemAdminWorkflowSearchResultActionsComponent { - /** - * The item to perform the actions on - */ - @Input() public item: Item; - - /** - * Whether or not to use small buttons - */ - @Input() public small: boolean; - - /** - * Returns the path to the edit page of this item - */ - getEditPath(): string { - return getItemEditPath(this.item.uuid) - } - - /** - * Returns the path to the withdraw page of this item - */ - getWithdrawPath(): string { - return new URLCombiner(this.getEditPath(), ITEM_EDIT_WITHDRAW_PATH).toString(); - } -} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html similarity index 100% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.html rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.scss similarity index 100% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.scss rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.scss diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts similarity index 98% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.spec.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts index c1d42341bc..2ee8ae2e9d 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts @@ -4,7 +4,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { ItemAdminWorkflowSearchResultActionsComponent } from './item-admin-workflow-search-result-actions.component'; +import { ItemAdminWorkflowSearchResultActionsComponent } from './workflow-item-admin-workflow-actions.component'; import { Item } from '../../../core/shared/item.model'; import { ITEM_EDIT_DELETE_PATH, diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts new file mode 100644 index 0000000000..a19f154f13 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts @@ -0,0 +1,39 @@ +import { Component, Input } from '@angular/core'; +import { getItemEditPath } from '../../../+item-page/item-page-routing.module'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { ITEM_EDIT_WITHDRAW_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; +import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; + +@Component({ + selector: 'ds-workflow-item-admin-workflow-actions-element', + styleUrls: ['./workflow-item-admin-workflow-actions.component.scss'], + templateUrl: './workflow-item-admin-workflow-actions.component.html' +}) +/** + * The component for displaying the actions for a list element for an item on the admin workflow page + */ +export class WorkflowItemAdminWorkflowActionsComponent { + /** + * The item to perform the actions on + */ + @Input() public wfi: WorkflowItem; + + /** + * Whether or not to use small buttons + */ + @Input() public small: boolean; + + /** + * Returns the path to the delete page of this workflow item + */ + getDeletePath(): string { + return getDeletePath(this.wfi.id) + } + + /** + * Returns the path to the send back page of this workflow item + */ + getSendBackPath(): string { + return getSendPath(this.wfi.id); + } +} diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts index d29d876d02..b829c8d868 100644 --- a/src/app/+admin/admin.module.ts +++ b/src/app/+admin/admin.module.ts @@ -12,10 +12,12 @@ import { ItemAdminSearchResultGridElementComponent } from './admin-search-page/a import { CommunityAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component'; import { CollectionAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component'; import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin-search-results/item-admin-search-result-actions.component'; -import { ItemAdminWorkflowSearchResultGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/item-search-result/item-admin-workflow-search-result-grid-element.component'; -import { ItemAdminWorkflowSearchResultActionsComponent } from './admin-workflow-page/admin-workflow-search-results/item-admin-workflow-search-result-actions.component'; -import { ItemAdminWorkflowSearchResultListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/item-search-result/item-admin-workflow-search-result-list-element.component'; +import { WorkflowItemAdminWorkflowGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component'; +import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component'; +import { WorkflowItemAdminWorkflowListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; +import { PoolTaskAdminWorkflowSearchResultListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component'; +import { PoolTaskAdminWorkflowSearchResultGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component'; @NgModule({ imports: [ @@ -36,9 +38,12 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow CollectionAdminSearchResultGridElementComponent, ItemAdminSearchResultActionsComponent, - ItemAdminWorkflowSearchResultListElementComponent, - ItemAdminWorkflowSearchResultGridElementComponent, - ItemAdminWorkflowSearchResultActionsComponent, + WorkflowItemAdminWorkflowListElementComponent, + WorkflowItemAdminWorkflowGridElementComponent, + WorkflowItemAdminWorkflowActionsComponent, + + PoolTaskAdminWorkflowSearchResultListElementComponent, + PoolTaskAdminWorkflowSearchResultGridElementComponent, ], entryComponents: [ ItemAdminSearchResultListElementComponent, @@ -49,9 +54,12 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow CollectionAdminSearchResultGridElementComponent, ItemAdminSearchResultActionsComponent, - ItemAdminWorkflowSearchResultListElementComponent, - ItemAdminWorkflowSearchResultGridElementComponent, - ItemAdminWorkflowSearchResultActionsComponent, + WorkflowItemAdminWorkflowListElementComponent, + WorkflowItemAdminWorkflowGridElementComponent, + WorkflowItemAdminWorkflowActionsComponent, + + PoolTaskAdminWorkflowSearchResultListElementComponent, + PoolTaskAdminWorkflowSearchResultGridElementComponent, ] }) export class AdminModule { diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html new file mode 100644 index 0000000000..f8d7e427b0 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html @@ -0,0 +1 @@ + diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.scss b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts new file mode 100644 index 0000000000..b42fe965c1 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; + +describe('WorkflowItemDeleteComponent', () => { + let component: WorkflowItemDeleteComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ WorkflowItemDeleteComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WorkflowItemDeleteComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts new file mode 100644 index 0000000000..2b3bb3e934 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; +import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; + +@Component({ + selector: 'ds-workflow-item-delete', + templateUrl: './workflow-item-delete.component.html', + styleUrls: ['./workflow-item-delete.component.scss'] +}) +export class WorkflowItemDeleteComponent implements OnInit { + wfi: WorkflowItem; + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts index 7a89f18c7d..8e301390ae 100644 --- a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '../shared/shared.module'; import { WorkflowItemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module'; import { SubmissionModule } from '../submission/submission.module'; +import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; @NgModule({ imports: [ @@ -11,7 +12,7 @@ import { SubmissionModule } from '../submission/submission.module'; SharedModule, SubmissionModule, ], - declarations: [] + declarations: [WorkflowItemDeleteComponent] }) /** * This module handles all modules that need to access the workflowitems edit page. From b603c7fc0556c45c0901591d5c07faa2699d3086 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Apr 2020 18:52:53 +0200 Subject: [PATCH 012/103] Added a component that show a paginated list of eperson or group --- .../eperson-group-list.component.html | 34 ++++ .../eperson-group-list.component.scss | 0 .../eperson-group-list.component.ts | 154 ++++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.html create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.scss create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.html b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.html new file mode 100644 index 0000000000..729236da93 --- /dev/null +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.html @@ -0,0 +1,34 @@ +
+ + +
+ + + + + + + + + + + + + + + +
{{'resource-policies.form.eperson-group-list.table.headers.id' | translate}}{{'resource-policies.form.eperson-group-list.table.headers.name' | translate}}{{'resource-policies.form.eperson-group-list.table.headers.action' | translate}}
{{entry.id}}{{dsoNameService.getName(entry)}} + +
+
+ +
+
diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.scss b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts new file mode 100644 index 0000000000..ed2f420438 --- /dev/null +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts @@ -0,0 +1,154 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; + +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { map, take } from 'rxjs/operators'; +import { uniqueId } from 'lodash' + +import { RemoteData } from '../../../../core/data/remote-data'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; +import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model'; +import { DataService } from '../../../../core/data/data.service'; +import { hasValue, isNotEmpty } from '../../../empty.util'; +import { FindListOptions } from '../../../../core/data/request.models'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; + +@Component({ + selector: 'ds-eperson-group-list', + styleUrls: ['./eperson-group-list.component.scss'], + templateUrl: './eperson-group-list.component.html' +}) +/** + * Component that shows a list of eperson or group + */ +export class EpersonGroupListComponent implements OnInit, OnDestroy { + + /** + * A boolean representing id component should list eperson or group + */ + @Input() isListOfEPerson = true; + + /** + * The uuid of eperson or group initially selected + */ + @Input() initSelected: string; + + /** + * An event fired when a eperson or group is selected. + * Event's payload equals to DSpaceObject. + */ + @Output() select: EventEmitter = new EventEmitter(); + + /** + * Pagination config used to display the list + */ + public paginationOptions: PaginationComponentOptions = new PaginationComponentOptions(); + + /** + * The data service used to make request. + * It could be EPersonDataService or GroupDataService + */ + private dataService: DataService; + + /** + * A list of eperson or group + */ + private list$: BehaviorSubject>> = new BehaviorSubject>>({} as any); + + /** + * The eperson or group's id selected + * @type {string} + */ + private entrySelectedId: BehaviorSubject = new BehaviorSubject(''); + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + private subs: Subscription[] = []; + + constructor(public dsoNameService: DSONameService, + private epersonService: EPersonDataService, + private groupsService: GroupDataService) { + } + + /** + * Initialize the component + */ + ngOnInit(): void { + this.paginationOptions.id = uniqueId('eperson-group-list-pagination'); + this.paginationOptions.pageSize = 5; + this.dataService = (this.isListOfEPerson) ? this.epersonService : this.groupsService; + + if (this.initSelected) { + this.entrySelectedId.next(this.initSelected); + } + + this.updateList(this.paginationOptions); + } + + /** + * Method called when an entry is selected. + * Emit a new select Event + * + * @param entry The eperson or group selected + */ + emitSelect(entry: DSpaceObject): void { + this.select.emit(entry); + this.entrySelectedId.next(entry.id); + } + + /** + * Return the list of eperson or group + */ + getList(): Observable>> { + return this.list$.asObservable(); + } + + /** + * Return a boolean representing if a table row is selected + * + * @return {boolean} + */ + isSelected(entry: DSpaceObject): Observable { + return this.entrySelectedId.asObservable().pipe( + map((selectedId) => isNotEmpty(selectedId) && selectedId === entry.id) + ) + } + + /** + * Method called on page change + */ + onPageChange(page: number): void { + this.paginationOptions.currentPage = page; + this.updateList(this.paginationOptions); + } + + /** + * Retrieve a paginate list of eperson or group + */ + updateList(config: PaginationComponentOptions): void { + const options: FindListOptions = Object.assign({}, new FindListOptions(), { + elementsPerPage: config.pageSize, + currentPage: config.currentPage + }); + + this.subs.push(this.dataService.findAll(options).pipe(take(1)) + .subscribe((list: RemoteData>) => { + this.list$.next(list) + }) + ); + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()) + } + +} From 8913a45a6be84c7b78812fb8c305e2b9dca9b8a6 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Apr 2020 18:54:34 +0200 Subject: [PATCH 013/103] Added a form field for selected resource policy target eperson/group --- .../form/resource-policy-form.html | 39 ++++ .../form/resource-policy-form.model.ts | 148 ++++++++++++ .../form/resource-policy-form.ts | 220 ++++++++++++++++++ 3 files changed, 407 insertions(+) create mode 100644 src/app/shared/resource-policies/form/resource-policy-form.html create mode 100644 src/app/shared/resource-policies/form/resource-policy-form.model.ts create mode 100644 src/app/shared/resource-policies/form/resource-policy-form.ts diff --git a/src/app/shared/resource-policies/form/resource-policy-form.html b/src/app/shared/resource-policies/form/resource-policy-form.html new file mode 100644 index 0000000000..62b7ead932 --- /dev/null +++ b/src/app/shared/resource-policies/form/resource-policy-form.html @@ -0,0 +1,39 @@ +
+ +
+ + + + + + + + + + + + + + +
+
+
+ +
+ + +
+
+
+
+
diff --git a/src/app/shared/resource-policies/form/resource-policy-form.model.ts b/src/app/shared/resource-policies/form/resource-policy-form.model.ts new file mode 100644 index 0000000000..3192946c9b --- /dev/null +++ b/src/app/shared/resource-policies/form/resource-policy-form.model.ts @@ -0,0 +1,148 @@ +import { + DynamicDateControlModelConfig, + DynamicFormControlLayout, + DynamicFormGroupModelConfig, + DynamicFormOptionConfig, + DynamicSelectModelConfig, +} from '@ng-dynamic-forms/core'; + +import { DsDynamicInputModelConfig } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { DsDynamicTextAreaModelConfig } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; +import { PolicyType } from '../../../core/resource-policy/models/policy-type.model'; +import { ActionType } from '../../../core/resource-policy/models/action-type.model'; + +const policyTypeList: Array> = [ + { + label: PolicyType.TYPE_SUBMISSION, + value: PolicyType.TYPE_SUBMISSION + }, + { + label: PolicyType.TYPE_WORKFLOW, + value: PolicyType.TYPE_WORKFLOW + }, + { + label: PolicyType.TYPE_INHERITED, + value: PolicyType.TYPE_INHERITED + }, + { + label: PolicyType.TYPE_CUSTOM, + value: PolicyType.TYPE_CUSTOM + }, +]; + +const policyActionList: Array> = [ + { + label: ActionType.READ.toString(), + value: ActionType.READ + }, + { + label: ActionType.WRITE.toString(), + value: ActionType.WRITE + }, + { + label: ActionType.REMOVE.toString(), + value: ActionType.REMOVE + }, + { + label: ActionType.ADMIN.toString(), + value: ActionType.ADMIN + }, + { + label: ActionType.DELETE.toString(), + value: ActionType.DELETE + }, + { + label: ActionType.WITHDRAWN_READ.toString(), + value: ActionType.WITHDRAWN_READ + }, + { + label: ActionType.DEFAULT_BITSTREAM_READ.toString(), + value: ActionType.DEFAULT_BITSTREAM_READ + }, + { + label: ActionType.DEFAULT_ITEM_READ.toString(), + value: ActionType.DEFAULT_ITEM_READ + } +]; + +export const RESOURCE_POLICY_FORM_NAME_CONFIG: DsDynamicInputModelConfig = { + id: 'name', + label: 'resource-policies.form.name.label', + metadataFields: [], + repeatable: false, + submissionId: '' +}; + +export const RESOURCE_POLICY_FORM_DESCRIPTION_CONFIG: DsDynamicTextAreaModelConfig = { + id: 'description', + label: 'resource-policies.form.description.label', + metadataFields: [], + repeatable: false, + rows: 10, + submissionId: '' +}; + +export const RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG: DynamicSelectModelConfig = { + id: 'policyType', + label: 'resource-policies.form.policy-type.label', + options: policyTypeList, + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'resource-policies.form.policy-type.required' + } +}; + +export const RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG: DynamicSelectModelConfig = { + id: 'action', + label: 'resource-policies.form.action-type.label', + options: policyActionList, + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'resource-policies.form.action-type.required' + } +}; + +export const RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG: DynamicFormGroupModelConfig = { + id: 'date', + group: [] +}; +export const RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT: DynamicFormControlLayout = { + element: { + control: 'form-row', + } +}; + +export const RESOURCE_POLICY_FORM_START_DATE_CONFIG: DynamicDateControlModelConfig = { + id: 'start', + label: 'resource-policies.form.date.start.label', +}; + +export const RESOURCE_POLICY_FORM_START_DATE_LAYOUT: DynamicFormControlLayout = { + element: { + container: 'p-0', + label: 'col-form-label' + }, + grid: { + host: 'col-md-6' + } +}; + +export const RESOURCE_POLICY_FORM_END_DATE_CONFIG: DynamicDateControlModelConfig = { + id: 'end', + label: 'resource-policies.form.date.end.label' +}; +export const RESOURCE_POLICY_FORM_END_DATE_LAYOUT: DynamicFormControlLayout = { + element: { + container: 'p-0', + label: 'col-form-label' + }, + grid: { + host: 'col-md-6' + } +}; diff --git a/src/app/shared/resource-policies/form/resource-policy-form.ts b/src/app/shared/resource-policies/form/resource-policy-form.ts new file mode 100644 index 0000000000..9c08e8dcb3 --- /dev/null +++ b/src/app/shared/resource-policies/form/resource-policy-form.ts @@ -0,0 +1,220 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { DynamicFormControlModel, DynamicFormGroupModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; + +import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; +import { DsDynamicInputModel } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { + RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG, + RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG, + RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT, + RESOURCE_POLICY_FORM_DESCRIPTION_CONFIG, + RESOURCE_POLICY_FORM_END_DATE_CONFIG, + RESOURCE_POLICY_FORM_END_DATE_LAYOUT, + RESOURCE_POLICY_FORM_NAME_CONFIG, + RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG, + RESOURCE_POLICY_FORM_START_DATE_CONFIG, + RESOURCE_POLICY_FORM_START_DATE_LAYOUT +} from './resource-policy-form.model'; +import { DsDynamicTextAreaModel } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; +import { DynamicDsDatePickerModel } from '../../form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { hasValue, isNotEmpty } from '../../empty.util'; +import { FormService } from '../../form/form.service'; + +export interface ResourcePolicyEvent { + object: ResourcePolicy, + target: { + type: string, + uuid: string + } +} + +@Component({ + selector: 'ds-resource-policy-form', + templateUrl: './resource-policy-form.html', +}) +/** + * Component that show form for adding/editing a resource policy + */ +export class ResourcePolicyFormComponent implements OnInit { + + /** + * The resource policy to edit + * @type {ResourcePolicy} + */ + @Input() resourcePolicy: ResourcePolicy; + + /** + * An event fired when form is canceled. + * Event's payload is empty. + */ + @Output() reset: EventEmitter = new EventEmitter(); + + /** + * An event fired when form is submitted. + * Event's payload equals to a new ResourcePolicy. + */ + @Output() submit: EventEmitter = new EventEmitter(); + + /** + * The form id + * @type {string} + */ + public formId: string; + + /** + * The form model + * @type {DynamicFormControlModel[]} + */ + public formModel: DynamicFormControlModel[]; + + /** + * The eperson or group that will be grant of the permission + * @type {DSpaceObject} + */ + public resourcePolicyTarget: DSpaceObject; + + /** + * The type of the object that will be grant of the permission. It could be 'eperson' or 'group' + * @type {string} + */ + public resourcePolicyTargetType: string; + + /** + * Initialize instance variables + * + * @param {DSONameService} dsoNameService + * @param {FormService} formService + */ + constructor( + private dsoNameService: DSONameService, + private formService: FormService, + ) { + } + + /** + * Initialize the component, setting up the form model + */ + ngOnInit(): void { + this.formId = this.formService.getUniqueId('resource-policy-form'); + this.formModel = this.buildResourcePolicyForm(); + } + + /** + * Method to check if the form status is valid or not + * + * @return Observable that emits the form status + */ + isFormValid(): Observable { + return this.formService.isValid(this.formId).pipe( + map((isValid: boolean) => isValid && isNotEmpty(this.resourcePolicyTarget)) + ) + } + + /** + * Initialize the form model + * + * @return the form models + */ + private buildResourcePolicyForm(): DynamicFormControlModel[] { + const formModel: DynamicFormControlModel[] = []; + formModel.push( + new DsDynamicInputModel(RESOURCE_POLICY_FORM_NAME_CONFIG), + new DsDynamicTextAreaModel(RESOURCE_POLICY_FORM_DESCRIPTION_CONFIG), + new DynamicSelectModel(RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG), + new DynamicSelectModel(RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG) + ); + + const startDateModel = new DynamicDsDatePickerModel( + RESOURCE_POLICY_FORM_START_DATE_CONFIG, + RESOURCE_POLICY_FORM_START_DATE_LAYOUT + ); + const endDateModel = new DynamicDsDatePickerModel( + RESOURCE_POLICY_FORM_END_DATE_CONFIG, + RESOURCE_POLICY_FORM_END_DATE_LAYOUT + ); + const dateGroupConfig = Object.assign({}, RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG); + dateGroupConfig.group.push(startDateModel, endDateModel); + formModel.push(new DynamicFormGroupModel(dateGroupConfig, RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT)); + + this.initModelsValue(formModel); + return formModel + } + + /** + * Setting up the form models value + * + * @return the form models + */ + initModelsValue(formModel: DynamicFormControlModel[]): DynamicFormControlModel[] { + if (this.resourcePolicy) { + formModel.forEach((model: any) => { + if (model.id === 'date') { + if (hasValue(this.resourcePolicy.startDate)) { + model.get(0).valueUpdates.next(this.resourcePolicy.startDate); + } + if (hasValue(this.resourcePolicy.endDate)) { + model.get(1).valueUpdates.next(this.resourcePolicy.startDate); + } + } else { + if (this.resourcePolicy.hasOwnProperty(model.id) && this.resourcePolicy[model.id]) { + model.valueUpdates.next(this.resourcePolicy[model.id]); + } + } + }) + } + + return formModel; + } + + /** + * Return the name of the eperson or group that will be grant of the permission + * + * @return the object name + */ + getResourcePolicyTargetName(): string { + return isNotEmpty(this.resourcePolicyTarget) ? this.dsoNameService.getName(this.resourcePolicyTarget) : ''; + } + + /** + * Update reference to the eperson or group that will be grant of the permission + */ + updateObjectSelected(object: DSpaceObject, isEPerson: boolean): void { + this.resourcePolicyTarget = object; + this.resourcePolicyTargetType = isEPerson ? 'eperson' : 'group'; + } + + /** + * Method called on reset + * Emit a new reset Event + */ + onReset(): void { + this.reset.emit(); + } + + /** + * Method called on submit. + * Emit a new submit Event whether the form is valid + */ + onSubmit(): void { + this.formService.getFormData(this.formId) + .subscribe((data) => { + const eventPayload: ResourcePolicyEvent = Object.create({}); + const resourcePolicy = new ResourcePolicy(); + resourcePolicy.name = data.name; + resourcePolicy.description = data.description; + resourcePolicy.policyType = data.policyType; + resourcePolicy.action = data.action; + resourcePolicy.startDate = (data.date) ? data.date.start : undefined; + resourcePolicy.endDate = (data.date) ? data.date.end : undefined; + eventPayload.object = resourcePolicy; + eventPayload.target.type = this.resourcePolicyTargetType; + eventPayload.target.uuid = this.resourcePolicyTarget.id; + this.submit.emit(eventPayload); + }) + } +} From 7f6c88164b3784bf244bf724b4851efb95ab780c Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Apr 2020 18:57:48 +0200 Subject: [PATCH 014/103] Added create and edit resource policy link --- .../resource-policies.component.html | 18 ++++-- .../resource-policies.component.ts | 56 +++++++++++++++++-- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index dbcf3a45e7..ecf810fbf8 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -2,10 +2,10 @@ - + @@ -22,13 +23,22 @@ - + + + diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts index 0596dba586..1f33437bfd 100644 --- a/src/app/shared/resource-policies/resource-policies.component.ts +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -1,5 +1,5 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; import { Observable, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -13,6 +13,8 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { Group } from '../../core/eperson/models/group.model'; import { GroupDataService } from '../../core/eperson/group-data.service'; import { hasValue, isNotEmpty } from '../empty.util'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; @Component({ selector: 'ds-resource-policies', @@ -51,15 +53,21 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { /** * Initialize instance variables * + * @param {ChangeDetectorRef} cdr * @param {DSONameService} dsoNameService + * @param {EPersonDataService} ePersonService * @param {GroupDataService} groupService * @param {ResourcePolicyService} resourcePolicyService + * @param {ActivatedRoute} route * @param {Router} router */ constructor( + private cdr: ChangeDetectorRef, private dsoNameService: DSONameService, + private ePersonService: EPersonDataService, private groupService: GroupDataService, private resourcePolicyService: ResourcePolicyService, + private route: ActivatedRoute, private router: Router ) { } @@ -74,6 +82,34 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { } + /** + * Redirect to resource policy creation page + */ + createResourcePolicy(): void { + this.router.navigate([`../${this.resourceUUID}/create`], { relativeTo: this.route }) + } + + /** + * Redirect to resource policy editing page + * + * @param policy The resource policy + */ + editResourcePolicy(policy: ResourcePolicy): void { + this.router.navigate([`../${this.resourceUUID}/${policy.id}/edit`], { relativeTo: this.route }) + } + + /** + * Return the ePerson's name which the given policy is linked to + * + * @param policy The resource policy + */ + getEPersonName(policy: ResourcePolicy): Observable { + return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + getFirstSucceededRemoteDataPayload(), + map((eperson: EPerson) => this.dsoNameService.getName(eperson)) + ) + } + /** * Return the group's name which the given policy is linked to * @@ -82,8 +118,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { getGroupName(policy: ResourcePolicy): Observable { return this.groupService.findByHref(policy._links.group.href).pipe( getFirstSucceededRemoteDataPayload(), - // A group has not dc.title metadata so is not possible to use DSONameService to retrieve name - map((group: Group) => group.name) + map((group: Group) => this.dsoNameService.getName(group)) ) } @@ -96,6 +131,19 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { return this.resourcePolicies$; } + /** + * Check whether the given policy is linked to a ePerson + * + * @param policy The resource policy + * @return an observable that emits true when the policy is linked to a ePerson, false otherwise + */ + hasEPerson(policy): Observable { + return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + getFirstSucceededRemoteDataPayload(), + map((eperson: EPerson) => isNotEmpty(eperson)) + ) + } + /** * Check whether the given policy is linked to a group * From fab11e90556164fb6310f1305016e97f8dfa1f47 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Apr 2020 18:58:51 +0200 Subject: [PATCH 015/103] Added component to edit and create a resource policy --- resources/i18n/en.json5 | 40 ++++++++++++++++++- .../edit-item-page.routing.module.ts | 17 +++++++- .../resource-policy-create.component.html | 6 +++ .../resource-policy-create.component.ts | 32 +++++++++++++++ .../edit/resource-policy-edit.component.html | 5 +++ .../edit/resource-policy-edit.component.ts | 9 +++++ src/app/shared/shared.module.ts | 12 +++++- 7 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/resource-policies/create/resource-policy-create.component.html create mode 100644 src/app/shared/resource-policies/create/resource-policy-create.component.ts create mode 100644 src/app/shared/resource-policies/edit/resource-policy-edit.component.html create mode 100644 src/app/shared/resource-policies/edit/resource-policy-edit.component.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index e85c374779..c869f51e5f 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1667,15 +1667,51 @@ "resource-policies.add.for.item": "Add a new Item policy", + "resource-policies.create.modal.head": "Create new resource policy", + + "resource-policies.edit.modal.head": "Edit resource policy", + + "resource-policies.form.action-type.label": "Select the action type", + + "resource-policies.form.action-type.required": "You must select the resource policy action.", + + "resource-policies.form.eperson-group-list.label": "Select the eperson or group that will be grant of the permission", + + "resource-policies.form.eperson-group-list.select.btn": "Select", + + "resource-policies.form.eperson-group-list.tab.eperson": "Search for a ePerson", + + "resource-policies.form.eperson-group-list.tab.group": "Search for a group", + + "resource-policies.form.eperson-group-list.table.headers.action": "Action", + + "resource-policies.form.eperson-group-list.table.headers.id": "ID", + + "resource-policies.form.eperson-group-list.table.headers.name": "Name", + + "resource-policies.form.date.end.label": "End Date", + + "resource-policies.form.date.start.label": "Start Date", + + "resource-policies.form.description.label": "Description", + + "resource-policies.form.name.label": "Name", + + "resource-policies.form.policy-type.label": "Select the policy type", + + "resource-policies.form.policy-type.required": "You must select the resource policy type.", + "resource-policies.table.headers.action": "Action", "resource-policies.table.headers.date.end": "End Date", "resource-policies.table.headers.date.start": "Start Date", - "resource-policies.table.headers.group": "Group", + "resource-policies.table.headers.edit": "Edit", - "resource-policies.table.headers.group.edit": "Edit", + "resource-policies.table.headers.eperson": "EPerson", + + "resource-policies.table.headers.group": "Group", "resource-policies.table.headers.name": "Name", diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index b41df21eaf..c9bb14b1a9 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -15,6 +15,8 @@ import { ItemRelationshipsComponent } from './item-relationships/item-relationsh import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; +import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; +import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; export const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; @@ -116,8 +118,21 @@ export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations'; }, { path: ITEM_EDIT_AUTHORIZATIONS_PATH, - component: ItemAuthorizationsComponent, data: { title: 'item.edit.authorizations.title' }, + children: [ + { + path: ':dso/create', + component: ResourcePolicyCreateComponent, + }, + { + path: ':dso/:policy/edit', + component: ResourcePolicyEditComponent, + }, + { + path: '', + component: ItemAuthorizationsComponent + } + ] } ] } diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.html b/src/app/shared/resource-policies/create/resource-policy-create.component.html new file mode 100644 index 0000000000..7990b8eb43 --- /dev/null +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.html @@ -0,0 +1,6 @@ +
+

{{'resource-policies.create.modal.head' | translate}}

+ + +
diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.ts b/src/app/shared/resource-policies/create/resource-policy-create.component.ts new file mode 100644 index 0000000000..e92dab14da --- /dev/null +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +import { take } from 'rxjs/operators'; + +import { ResourcePolicyEvent } from '../form/resource-policy-form'; +import { RouteService } from '../../../core/services/route.service'; +import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; + +@Component({ + selector: 'ds-resource-policy-create', + templateUrl: './resource-policy-create.component.html' +}) +export class ResourcePolicyCreateComponent { + + constructor( + protected resourcePolicy: ResourcePolicyService, + protected router: Router, + protected routeService: RouteService) { + } + + createResourcePolicy(event: ResourcePolicyEvent) { + + } + + redirectToPreviousPage() { + this.routeService.getPreviousUrl().pipe(take(1)) + .subscribe((url) => { + this.router.navigateByUrl(url); + }) + } +} diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.html b/src/app/shared/resource-policies/edit/resource-policy-edit.component.html new file mode 100644 index 0000000000..d463e7fc1a --- /dev/null +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.html @@ -0,0 +1,5 @@ +
+

{{'resource-policies.edit.modal.head' | translate}}

+ + +
diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts new file mode 100644 index 0000000000..fa55979d35 --- /dev/null +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-resource-policy-edit', + templateUrl: './resource-policy-edit.component.html' +}) +export class ResourcePolicyEditComponent { + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 313c56089b..86ae893057 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -181,6 +181,10 @@ import { SortablejsModule } from 'ngx-sortablejs'; import { MissingTranslationHelper } from './translate/missing-translation.helper'; import { ResourcePoliciesComponent } from './resource-policies/resource-policies.component'; import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive'; +import { ResourcePolicyFormComponent } from './resource-policies/form/resource-policy-form'; +import { ResourcePolicyCreateComponent } from './resource-policies/create/resource-policy-create.component'; +import { ResourcePolicyEditComponent } from './resource-policies/edit/resource-policy-edit.component'; +import { EpersonGroupListComponent } from './resource-policies/form/eperson-group-list/eperson-group-list.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -349,7 +353,11 @@ const COMPONENTS = [ ExistingMetadataListElementComponent, ItemVersionsComponent, PublicationSearchResultListElementComponent, - ResourcePoliciesComponent + ResourcePoliciesComponent, + ResourcePolicyFormComponent, + ResourcePolicyCreateComponent, + ResourcePolicyEditComponent, + EpersonGroupListComponent ]; const ENTRY_COMPONENTS = [ @@ -414,6 +422,8 @@ const ENTRY_COMPONENTS = [ DsDynamicLookupRelationExternalSourceTabComponent, ExternalSourceEntryImportModalComponent, ItemVersionsComponent, + ResourcePolicyCreateComponent, + ResourcePolicyEditComponent ]; const SHARED_ITEM_PAGE_COMPONENTS = [ From 4f14e546a5ddb054ad70aca43427639b3eb9d919 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Apr 2020 19:10:22 +0200 Subject: [PATCH 016/103] Added fallback strategy for DSONameService to get name for dspace object without a dc.tile metadata --- src/app/core/breadcrumbs/dso-name.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/core/breadcrumbs/dso-name.service.ts b/src/app/core/breadcrumbs/dso-name.service.ts index 161c4f7254..5567137334 100644 --- a/src/app/core/breadcrumbs/dso-name.service.ts +++ b/src/app/core/breadcrumbs/dso-name.service.ts @@ -28,7 +28,8 @@ export class DSONameService { return dso.firstMetadataValue('organization.legalName'); }, Default: (dso: DSpaceObject): string => { - return dso.firstMetadataValue('dc.title'); + // If object doesn't have dc.title metadata use name property + return dso.firstMetadataValue('dc.title') || dso.name; } }; From 41d6255998004610bfa46b533103b464c684817b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Apr 2020 19:19:38 +0200 Subject: [PATCH 017/103] Renamed SearchParam class with a generic name RequestParam --- ...-param.model.ts => request-param.model.ts} | 2 +- src/app/core/data/collection-data.service.ts | 4 ++-- src/app/core/data/data.service.ts | 4 ++-- src/app/core/data/relationship.service.ts | 4 ++-- src/app/core/data/request.models.ts | 4 ++-- .../core/eperson/eperson-data.service.spec.ts | 10 +++++----- src/app/core/eperson/eperson-data.service.ts | 8 ++++---- src/app/core/eperson/group-data.service.ts | 4 ++-- .../resource-policy.service.spec.ts | 20 +++++++++---------- .../resource-policy.service.ts | 14 ++++++------- 10 files changed, 37 insertions(+), 37 deletions(-) rename src/app/core/cache/models/{search-param.model.ts => request-param.model.ts} (86%) diff --git a/src/app/core/cache/models/search-param.model.ts b/src/app/core/cache/models/request-param.model.ts similarity index 86% rename from src/app/core/cache/models/search-param.model.ts rename to src/app/core/cache/models/request-param.model.ts index 3881dbe8b7..ac21fe0b8a 100644 --- a/src/app/core/cache/models/search-param.model.ts +++ b/src/app/core/cache/models/request-param.model.ts @@ -2,7 +2,7 @@ /** * Class representing a query parameter (query?fieldName=fieldValue) used in FindListOptions object */ -export class SearchParam { +export class RequestParam { constructor(public fieldName: string, public fieldValue: any) { } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 6ae40f4ca9..0639a7d8ca 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -12,7 +12,7 @@ import { PaginatedSearchOptions } from '../../shared/search/paginated-search-opt import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; @@ -94,7 +94,7 @@ export class CollectionDataService extends ComColDataService { getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable>> { const searchHref = 'findAuthorizedByCommunity'; options = Object.assign({}, options, { - searchParams: [new SearchParam('uuid', communityId)] + searchParams: [new RequestParam('uuid', communityId)] }); return this.searchBy(searchHref, options).pipe( diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index d3bca871be..892245006c 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -20,7 +20,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { getClassForType } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ErrorResponse, RestResponse } from '../cache/response.models'; @@ -110,7 +110,7 @@ export abstract class DataService { result$ = this.getSearchEndpoint(searchMethod); if (hasValue(options.searchParams)) { - options.searchParams.forEach((param: SearchParam) => { + options.searchParams.forEach((param: RequestParam) => { args.push(`${param.fieldName}=${param.fieldValue}`); }) } diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 4dde567c99..3d68e70206 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -21,7 +21,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; import { CoreState } from '../core.reducers'; @@ -257,7 +257,7 @@ export class RelationshipService extends DataService { if (options) { findListOptions = Object.assign(new FindListOptions(), options); } - const searchParams = [new SearchParam('label', label), new SearchParam('dso', item.id)]; + const searchParams = [new RequestParam('label', label), new RequestParam('dso', item.id)]; if (findListOptions.searchParams) { findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams]; } else { diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 0655333502..5866cce797 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -11,7 +11,7 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; import { RestRequestMethod } from './rest-request-method'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service'; import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service'; import { MetadataschemaParsingService } from './metadataschema-parsing.service'; @@ -146,7 +146,7 @@ export class FindListOptions { elementsPerPage?: number; currentPage?: number; sort?: SortOptions; - searchParams?: SearchParam[]; + searchParams?: RequestParam[]; startsWith?: string; } diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index 1831386321..f05a18bbd2 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -18,7 +18,7 @@ import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson-mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { ChangeAnalyzer } from '../data/change-analyzer'; @@ -105,7 +105,7 @@ describe('EPersonDataService', () => { it('search by default scope (byMetadata) and no query', () => { service.searchByScope(null, ''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', ''))] + searchParams: [Object.assign(new RequestParam('query', ''))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); @@ -113,7 +113,7 @@ describe('EPersonDataService', () => { it('search metadata scope and no query', () => { service.searchByScope('metadata', ''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', ''))] + searchParams: [Object.assign(new RequestParam('query', ''))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); @@ -121,7 +121,7 @@ describe('EPersonDataService', () => { it('search metadata scope and with query', () => { service.searchByScope('metadata', 'test'); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', 'test'))] + searchParams: [Object.assign(new RequestParam('query', 'test'))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); @@ -129,7 +129,7 @@ describe('EPersonDataService', () => { it('search email scope and no query', () => { service.searchByScope('email', ''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('email', ''))] + searchParams: [Object.assign(new RequestParam('email', ''))] }); expect(service.searchBy).toHaveBeenCalledWith('byEmail', options); }); diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index ec8b96d1cd..99514144d2 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -15,7 +15,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; import { DataService } from '../data/data.service'; @@ -97,7 +97,7 @@ export class EPersonDataService extends DataService { * @param linksToFollow */ private getEpeopleByEmail(query: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { - const searchParams = [new SearchParam('email', query)]; + const searchParams = [new RequestParam('email', query)]; return this.getEPeopleBy(searchParams, this.searchByEmailPath, options, ...linksToFollow); } @@ -108,7 +108,7 @@ export class EPersonDataService extends DataService { * @param linksToFollow */ private getEpeopleByMetadata(query: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { - const searchParams = [new SearchParam('query', query)]; + const searchParams = [new RequestParam('query', query)]; return this.getEPeopleBy(searchParams, this.searchByMetadataPath, options, ...linksToFollow); } @@ -119,7 +119,7 @@ export class EPersonDataService extends DataService { * @param options * @param linksToFollow */ - private getEPeopleBy(searchParams: SearchParam[], searchMethod: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + private getEPeopleBy(searchParams: RequestParam[], searchMethod: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { let findListOptions = new FindListOptions(); if (options) { findListOptions = Object.assign(new FindListOptions(), options); diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 532f42323a..40bd2fa275 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -13,7 +13,7 @@ import { Group } from './models/group.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { RemoteData } from '../data/remote-data'; import { PaginatedList } from '../data/paginated-list'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -56,7 +56,7 @@ export class GroupDataService extends DataService { isMemberOf(groupName: string): Observable { const searchHref = 'isMemberOf'; const options = new FindListOptions(); - options.searchParams = [new SearchParam('groupName', groupName)]; + options.searchParams = [new RequestParam('groupName', groupName)]; return this.searchBy(searchHref, options).pipe( filter((groups: RemoteData>) => !groups.isResponsePending), diff --git a/src/app/core/resource-policy/resource-policy.service.spec.ts b/src/app/core/resource-policy/resource-policy.service.spec.ts index a2bfb52e01..3408663db3 100644 --- a/src/app/core/resource-policy/resource-policy.service.spec.ts +++ b/src/app/core/resource-policy/resource-policy.service.spec.ts @@ -13,7 +13,7 @@ import { ResourcePolicyService } from './resource-policy.service'; import { PolicyType } from './models/policy-type.model'; import { ActionType } from './models/action-type.model'; import { FindListOptions } from '../data/request.models'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { PageInfo } from '../shared/page-info.model'; import { PaginatedList } from '../data/paginated-list'; import { createSuccessfulRemoteDataObject } from '../../shared/testing/utils'; @@ -168,7 +168,7 @@ describe('ResourcePolicyService', () => { describe('searchByEPerson', () => { it('should proxy the call to dataservice.searchBy', () => { const options = new FindListOptions(); - options.searchParams = [new SearchParam('uuid', epersonUUID)]; + options.searchParams = [new RequestParam('uuid', epersonUUID)]; scheduler.schedule(() => service.searchByEPerson(epersonUUID)); scheduler.flush(); @@ -178,8 +178,8 @@ describe('ResourcePolicyService', () => { it('should proxy the call to dataservice.searchBy with additional search param', () => { const options = new FindListOptions(); options.searchParams = [ - new SearchParam('uuid', epersonUUID), - new SearchParam('resource', resourceUUID), + new RequestParam('uuid', epersonUUID), + new RequestParam('resource', resourceUUID), ]; scheduler.schedule(() => service.searchByEPerson(epersonUUID, resourceUUID)); scheduler.flush(); @@ -200,7 +200,7 @@ describe('ResourcePolicyService', () => { describe('searchByGroup', () => { it('should proxy the call to dataservice.searchBy', () => { const options = new FindListOptions(); - options.searchParams = [new SearchParam('uuid', groupUUID)]; + options.searchParams = [new RequestParam('uuid', groupUUID)]; scheduler.schedule(() => service.searchByGroup(groupUUID)); scheduler.flush(); @@ -210,8 +210,8 @@ describe('ResourcePolicyService', () => { it('should proxy the call to dataservice.searchBy with additional search param', () => { const options = new FindListOptions(); options.searchParams = [ - new SearchParam('uuid', groupUUID), - new SearchParam('resource', resourceUUID), + new RequestParam('uuid', groupUUID), + new RequestParam('resource', resourceUUID), ]; scheduler.schedule(() => service.searchByGroup(groupUUID, resourceUUID)); scheduler.flush(); @@ -232,7 +232,7 @@ describe('ResourcePolicyService', () => { describe('searchByResource', () => { it('should proxy the call to dataservice.searchBy', () => { const options = new FindListOptions(); - options.searchParams = [new SearchParam('uuid', resourceUUID)]; + options.searchParams = [new RequestParam('uuid', resourceUUID)]; scheduler.schedule(() => service.searchByResource(resourceUUID)); scheduler.flush(); @@ -243,8 +243,8 @@ describe('ResourcePolicyService', () => { const action = ActionType.READ; const options = new FindListOptions(); options.searchParams = [ - new SearchParam('uuid', resourceUUID), - new SearchParam('action', action), + new RequestParam('uuid', resourceUUID), + new RequestParam('action', action), ]; scheduler.schedule(() => service.searchByResource(resourceUUID, action)); scheduler.flush(); diff --git a/src/app/core/resource-policy/resource-policy.service.ts b/src/app/core/resource-policy/resource-policy.service.ts index b9dd131fbe..aef407e9e1 100644 --- a/src/app/core/resource-policy/resource-policy.service.ts +++ b/src/app/core/resource-policy/resource-policy.service.ts @@ -22,7 +22,7 @@ import { ChangeAnalyzer } from '../data/change-analyzer'; import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; import { PaginatedList } from '../data/paginated-list'; import { ActionType } from './models/action-type.model'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { isNotEmpty } from '../../shared/empty.util'; /* tslint:disable:max-classes-per-file */ @@ -108,9 +108,9 @@ export class ResourcePolicyService { */ searchByEPerson(UUID: string, resourceUUID?: string, ...linksToFollow: Array>): Observable>> { const options = new FindListOptions(); - options.searchParams = [new SearchParam('uuid', UUID)]; + options.searchParams = [new RequestParam('uuid', UUID)]; if (isNotEmpty(resourceUUID)) { - options.searchParams.push(new SearchParam('resource', resourceUUID)) + options.searchParams.push(new RequestParam('resource', resourceUUID)) } return this.dataService.searchBy(this.searchByEPersonMethod, options, ...linksToFollow) } @@ -124,9 +124,9 @@ export class ResourcePolicyService { */ searchByGroup(UUID: string, resourceUUID?: string, ...linksToFollow: Array>): Observable>> { const options = new FindListOptions(); - options.searchParams = [new SearchParam('uuid', UUID)]; + options.searchParams = [new RequestParam('uuid', UUID)]; if (isNotEmpty(resourceUUID)) { - options.searchParams.push(new SearchParam('resource', resourceUUID)) + options.searchParams.push(new RequestParam('resource', resourceUUID)) } return this.dataService.searchBy(this.searchByGroupMethod, options, ...linksToFollow) } @@ -140,9 +140,9 @@ export class ResourcePolicyService { */ searchByResource(UUID: string, action?: ActionType, ...linksToFollow: Array>): Observable>> { const options = new FindListOptions(); - options.searchParams = [new SearchParam('uuid', UUID)]; + options.searchParams = [new RequestParam('uuid', UUID)]; if (isNotEmpty(action)) { - options.searchParams.push(new SearchParam('action', action)) + options.searchParams.push(new RequestParam('action', action)) } return this.dataService.searchBy(this.searchByResourceMethod, options, ...linksToFollow) } From b9de6a7a7d2657f253871c7d9c2059ad61887d13 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Apr 2020 19:54:30 +0200 Subject: [PATCH 018/103] Changed DataService's create method in the way to accept array of request params --- src/app/core/data/data.service.ts | 35 ++++++++++++++++--- .../create-comcol-page.component.ts | 3 +- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 892245006c..5f68dddeca 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -152,6 +152,33 @@ export abstract class DataService { } } + /** + * Turn an array of RequestParam into a query string and combine it with the given HREF + * + * @param href The HREF to which the query string should be appended + * @param params Array with additional params to combine with query string + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * + * @return {Observable} + * Return an observable that emits created HREF + */ + protected buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: Array>): string { + + let args = []; + if (hasValue(params)) { + params.forEach((param: RequestParam) => { + args.push(`${param.fieldName}=${param.fieldValue}`); + }) + } + + args = this.addEmbedParams(args, ...linksToFollow); + + if (isNotEmpty(args)) { + return new URLCombiner(href, `?${args.join('&')}`).toString(); + } else { + return href; + } + } /** * Adds the embed options to the link for the request * @param args params for the query string @@ -379,15 +406,15 @@ export abstract class DataService { * * @param {DSpaceObject} dso * The object to create - * @param {string} parentUUID - * The UUID of the parent to create the new object under + * @param {RequestParam[]} params + * Array with additional params to combine with query string */ - create(dso: T, parentUUID: string): Observable> { + create(dso: T, ...params: RequestParam[]): Observable> { const requestId = this.requestService.generateRequestId(); const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), distinctUntilChanged(), - map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) + map((endpoint: string) => this.buildHrefWithParams(endpoint, params)) ); const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso); diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index e9373aff47..a8d6499cbd 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -13,6 +13,7 @@ import { getSucceededRemoteData } from '../../../core/shared/operators'; import { ResourceType } from '../../../core/shared/resource-type'; import { hasValue, isNotEmpty, isNotUndefined } from '../../empty.util'; import { NotificationsService } from '../../notifications/notifications.service'; +import { RequestParam } from '../../../core/cache/models/request-param.model'; /** * Component representing the create page for communities and collections @@ -76,7 +77,7 @@ export class CreateComColPageComponent implements const uploader = event.uploader; this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - this.dsoDataService.create(dso, uuid) + this.dsoDataService.create(dso, new RequestParam('parent', uuid)) .pipe(getSucceededRemoteData()) .subscribe((dsoRD: RemoteData) => { if (isNotUndefined(dsoRD)) { From 1ac52edbf7348f476b32902f03eb9504ef3e8eb5 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Apr 2020 20:50:02 +0200 Subject: [PATCH 019/103] Added create and delete method to resource policy service --- .../resource-policy.service.spec.ts | 77 ++++++++++++++++--- .../resource-policy.service.ts | 34 ++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/app/core/resource-policy/resource-policy.service.spec.ts b/src/app/core/resource-policy/resource-policy.service.spec.ts index 3408663db3..6c26973e02 100644 --- a/src/app/core/resource-policy/resource-policy.service.spec.ts +++ b/src/app/core/resource-policy/resource-policy.service.spec.ts @@ -1,4 +1,5 @@ import { HttpClient } from '@angular/common/http'; +import { async } from '@angular/core/testing'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; @@ -27,15 +28,16 @@ describe('ResourcePolicyService', () => { let rdbService: RemoteDataBuildService; let objectCache: ObjectCacheService; let halService: HALEndpointService; + let responseCacheEntry: RequestEntry; - const resourcePolicy = { + const resourcePolicy: any = { id: '1', name: null, description: null, policyType: PolicyType.TYPE_SUBMISSION, action: ActionType.READ, - startDate : null, - endDate : null, + startDate: null, + endDate: null, type: 'resourcepolicy', uuid: 'resource-policy-1', _links: { @@ -51,14 +53,14 @@ describe('ResourcePolicyService', () => { } }; - const anotherResourcePolicy = { + const anotherResourcePolicy: any = { id: '2', name: null, description: null, policyType: PolicyType.TYPE_SUBMISSION, action: ActionType.WRITE, - startDate : null, - endDate : null, + startDate: null, + endDate: null, type: 'resourcepolicy', uuid: 'resource-policy-2', _links: { @@ -82,12 +84,10 @@ describe('ResourcePolicyService', () => { const resourceUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a'; const pageInfo = new PageInfo(); - const array = [resourcePolicy, anotherResourcePolicy ]; + const array = [resourcePolicy, anotherResourcePolicy]; const paginatedList = new PaginatedList(pageInfo, array); const resourcePolicyRD = createSuccessfulRemoteDataObject(resourcePolicy); const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); - const responseCacheEntry = new RequestEntry(); - responseCacheEntry.response = new RestResponse(true, 200, 'Success'); beforeEach(() => { scheduler = getTestScheduler(); @@ -96,11 +96,15 @@ describe('ResourcePolicyService', () => { getEndpoint: cold('a', { a: endpointURL }) }); + responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, configure: true, removeByHrefSubstring: {}, getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: hot('a|', { @@ -125,12 +129,67 @@ describe('ResourcePolicyService', () => { comparator ); + spyOn((service as any).dataService, 'create').and.callThrough(); + spyOn((service as any).dataService, 'delete').and.callThrough(); spyOn((service as any).dataService, 'findById').and.callThrough(); spyOn((service as any).dataService, 'findByHref').and.callThrough(); spyOn((service as any).dataService, 'searchBy').and.callThrough(); spyOn((service as any).dataService, 'getSearchByHref').and.returnValue(observableOf(requestURL)); }); + describe('create', () => { + it('should proxy the call to dataservice.create with eperson UUID', () => { + scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, epersonUUID)); + const params = [ + new RequestParam('resource', resourceUUID), + new RequestParam('eperson', epersonUUID) + ]; + scheduler.flush(); + + expect((service as any).dataService.create).toHaveBeenCalledWith(resourcePolicy, ...params); + }); + + it('should proxy the call to dataservice.create with group UUID', () => { + scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, null, groupUUID)); + const params = [ + new RequestParam('resource', resourceUUID), + new RequestParam('group', groupUUID) + ]; + scheduler.flush(); + + expect((service as any).dataService.create).toHaveBeenCalledWith(resourcePolicy, ...params); + }); + + it('should return a RemoteData for the object with the given id', () => { + const result = service.create(resourcePolicy, resourceUUID, epersonUUID); + const expected = cold('a|', { + a: resourcePolicyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('delete', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + responseCacheEntry.completed = true; + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: hot('a', { a: responseCacheEntry }), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + })); + + it('should proxy the call to dataservice.create', () => { + scheduler.schedule(() => service.delete(resourcePolicyId)); + scheduler.flush(); + + expect((service as any).dataService.delete).toHaveBeenCalledWith(resourcePolicyId); + }); + }); + describe('findById', () => { it('should proxy the call to dataservice.findById', () => { scheduler.schedule(() => service.findById(resourcePolicyId)); diff --git a/src/app/core/resource-policy/resource-policy.service.ts b/src/app/core/resource-policy/resource-policy.service.ts index aef407e9e1..44938ec6a3 100644 --- a/src/app/core/resource-policy/resource-policy.service.ts +++ b/src/app/core/resource-policy/resource-policy.service.ts @@ -69,6 +69,40 @@ export class ResourcePolicyService { this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); } + /** + * Create a new ResourcePolicy on the server, and store the response + * in the object cache + * + * @param {ResourcePolicy} resourcePolicy + * The resource policy to create + * @param {string} resourceUUID + * The uuid of the resource target of the policy + * @param {string} epersonUUID + * The uuid of the eperson that will be grant of the permission. Exactly one of eperson or group is required + * @param {string} groupUUID + * The uuid of the group that will be grant of the permission. Exactly one of eperson or group is required + */ + create(resourcePolicy: ResourcePolicy, resourceUUID: string, epersonUUID?: string, groupUUID?: string): Observable> { + const params = []; + params.push(new RequestParam('resource', resourceUUID)); + if (isNotEmpty(epersonUUID)) { + params.push(new RequestParam('eperson', epersonUUID)); + } else if (isNotEmpty(groupUUID)) { + params.push(new RequestParam('group', groupUUID)); + } + return this.dataService.create(resourcePolicy, ...params); + } + + /** + * Delete an existing ResourcePolicy on the server + * + * @param resourcePolicyID The resource policy's id to be removed + * @return an observable that emits true when the deletion was successful, false when it failed + */ + delete(resourcePolicyID: string): Observable { + return this.dataService.delete(resourcePolicyID); + } + /** * Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on an href, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the {@link ResourcePolicy} From 2eb3d11cd2d8dae8eb1819a211cbcc632d894169 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 3 Apr 2020 16:00:52 +0200 Subject: [PATCH 020/103] debugging issue commit --- ...ow-search-result-grid-element.component.ts | 6 +- ...-search-result-list-element.component.html | 5 +- ...ow-search-result-list-element.component.ts | 16 ++++- ...admin-workflow-list-element.component.html | 3 +- ...m-admin-workflow-list-element.component.ts | 19 +++++- ...item-admin-workflow-actions.component.html | 4 +- ...w-item-admin-workflow-actions.component.ts | 8 +-- .../edit-item-page/edit-item-page.module.ts | 1 - .../workflow-item-delete.component.html | 7 ++- .../workflow-item-delete.component.ts | 48 ++++++++++++++- .../workflow-item-page.resolver.ts | 35 +++++++++++ .../workflowitems-edit-page-routing.module.ts | 61 ++++++++++++++++--- src/app/app-routing.module.ts | 8 ++- .../submission/workflowitem-data.service.ts | 40 +++++++++++- src/app/shared/shared.module.ts | 4 +- 15 files changed, 233 insertions(+), 32 deletions(-) create mode 100644 src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts index e386ec45ae..70659a3f03 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts @@ -25,8 +25,12 @@ import { Observable } from 'rxjs'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { RemoteData } from '../../../../../core/data/remote-data'; import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { SearchResult } from '../../../../../shared/search/search-result.model'; +import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; +import { ClaimedTaskSearchResult } from '../../../../../shared/object-collection/shared/claimed-task-search-result.model'; @listableObjectComponent(PoolTaskSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) +@listableObjectComponent(ClaimedTaskSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ selector: 'ds-pool-task-admin-workflow-search-result-grid-element', styleUrls: ['./pool-task-admin-workflow-search-result-grid-element.component.scss'], @@ -35,7 +39,7 @@ import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../ /** * The component for displaying a list element for an pool task search result on the admin search page */ -export class PoolTaskAdminWorkflowSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnInit { +export class PoolTaskAdminWorkflowSearchResultGridElementComponent extends SearchResultGridElementComponent, TaskObject> implements OnInit { public wfi$: Observable; ngOnInit(): void { diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html index e894eedb94..c60347b08a 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html @@ -1,6 +1,7 @@ - +> diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts index 8a6934e755..db23a3c4aa 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts @@ -4,13 +4,20 @@ import { listableObjectComponent } from '../../../../../shared/object-collection import { Context } from '../../../../../core/shared/context.model'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; import { PoolTaskSearchResult } from '../../../../../shared/object-collection/shared/pool-task-search-result.model'; -import { PoolTask } from '../../../../../core/tasks/models/pool-task-object.model'; import { Observable, pipe } from 'rxjs'; import { RemoteData } from '../../../../../core/data/remote-data'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { ClaimedTaskSearchResult } from '../../../../../shared/object-collection/shared/claimed-task-search-result.model'; +import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; +import { SearchResult } from '../../../../../shared/search/search-result.model'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { tap } from 'rxjs/operators'; @listableObjectComponent(PoolTaskSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) +@listableObjectComponent(ClaimedTaskSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) @Component({ selector: 'ds-pooltask-admin-workflow-search-result-list-element', styleUrls: ['./pool-task-admin-workflow-search-result-list-element.component.scss'], @@ -19,11 +26,16 @@ import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../ /** * The component for displaying a list element for an pool task search result on the admin search page */ -export class PoolTaskAdminWorkflowSearchResultListElementComponent extends SearchResultListElementComponent { +export class PoolTaskAdminWorkflowSearchResultListElementComponent extends SearchResultListElementComponent, TaskObject> { public wfi$: Observable; + constructor(private linkService: LinkService, protected truncatableService: TruncatableService) { + super(truncatableService); + } + ngOnInit(): void { super.ngOnInit(); + this.dso = this.linkService.resolveLink(this.dso, followLink('workflowitem')); this.wfi$ = (this.dso.workflowitem as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); } } diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html index 736aba2fce..f3502fe0ab 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html @@ -1,7 +1,8 @@
{{ "admin.workflow.item.workflow" | translate }}
- { +export class WorkflowItemAdminWorkflowListElementComponent extends AbstractListableElementComponent implements OnInit { + public item$: Observable; + constructor(private linkService: LinkService) { + super(); + } + + ngOnInit(): void { + this.object = this.linkService.resolveLink(this.object, followLink('item')); + this.item$ = (this.object.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); + } } diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html index 3f80b127a8..3004eb925c 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html @@ -1,7 +1,7 @@ - + {{"admin.search.item.delete" | translate}} - + {{"admin.search.item.send-back" | translate}} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts index a19f154f13..58f89e31e6 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts @@ -1,8 +1,6 @@ import { Component, Input } from '@angular/core'; -import { getItemEditPath } from '../../../+item-page/item-page-routing.module'; -import { URLCombiner } from '../../../core/url-combiner/url-combiner'; -import { ITEM_EDIT_WITHDRAW_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; +import { getWorkflowItemDeletePath, getWorkflowItemSendBackPath } from '../../../+workflowitems-edit-page/workflowitems-edit-page-routing.module'; @Component({ selector: 'ds-workflow-item-admin-workflow-actions-element', @@ -27,13 +25,13 @@ export class WorkflowItemAdminWorkflowActionsComponent { * Returns the path to the delete page of this workflow item */ getDeletePath(): string { - return getDeletePath(this.wfi.id) + return getWorkflowItemDeletePath(this.wfi.id) } /** * Returns the path to the send back page of this workflow item */ getSendBackPath(): string { - return getSendPath(this.wfi.id); + return getWorkflowItemSendBackPath(this.wfi.id); } } diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index 2cbd0c57d1..b6aaf5faac 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -38,7 +38,6 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version EditItemPageComponent, ItemOperationComponent, AbstractSimpleItemActionComponent, - ModifyItemOverviewComponent, ItemWithdrawComponent, ItemReinstateComponent, ItemPrivateComponent, diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html index f8d7e427b0..5b1b8c45b3 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html @@ -1 +1,6 @@ - +
+ + + + +
diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts index 2b3bb3e934..4c6f0ff885 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts @@ -1,5 +1,15 @@ import { Component, OnInit } from '@angular/core'; import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; +import { Item } from '../../core/shared/item.model'; +import { Observable } from 'rxjs'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../core/shared/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { ActivatedRoute, Data, Router } from '@angular/router'; +import { map, switchMap, take } from 'rxjs/operators'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { RouteService } from '../../core/services/route.service'; @Component({ selector: 'ds-workflow-item-delete', @@ -7,10 +17,42 @@ import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; styleUrls: ['./workflow-item-delete.component.scss'] }) export class WorkflowItemDeleteComponent implements OnInit { - wfi: WorkflowItem; - constructor() { } + public wfi$: Observable; + public item$: Observable; - ngOnInit() { + constructor(private route: ActivatedRoute, + private workflowItemService: WorkflowItemDataService, + private router: Router, + private routeService: RouteService, + private notificationsService: NotificationsService, + private translationService: TranslateService) { } + ngOnInit() { + this.route.data.subscribe((t) => console.log(t)); + this.wfi$ = this.route.data.pipe(map((data: Data) => data.wfi as RemoteData), getRemoteDataPayload()); + this.item$ = this.wfi$.pipe(switchMap((wfi: WorkflowItem) => (wfi.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + } + + delete() { + this.wfi$.pipe( + take(1), + switchMap((wfi: WorkflowItem) => this.workflowItemService.delete(wfi.id)) + ).subscribe((successful: boolean) => { + if (successful) { + const title = this.translationService.get('workflowitem.delete.notification.success.title'); + const content = this.translationService.get('workflowitem.delete.notification.success.content'); + this.notificationsService.success(title, content) + } else { + const title = this.translationService.get('workflowitem.delete.notification.error.title'); + const content = this.translationService.get('workflowitem.delete.notification.error.content'); + this.notificationsService.error(title, content) + } + this.previousPage(); + }) + } + + previousPage() { + this.routeService.getPreviousUrl(); + } } diff --git a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts new file mode 100644 index 0000000000..5ae11efff6 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../core/data/remote-data'; +import { ItemDataService } from '../core/data/item-data.service'; +import { Item } from '../core/shared/item.model'; +import { hasValue } from '../shared/empty.util'; +import { find } from 'rxjs/operators'; +import { followLink } from '../shared/utils/follow-link-config.model'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; +import { WorkflowItem } from '../core/submission/models/workflowitem.model'; + +/** + * This class represents a resolver that requests a specific item before the route is activated + */ +@Injectable() +export class WorkflowItemPageResolver implements Resolve> { + constructor(private workflowItemService: WorkflowItemDataService) { + } + + /** + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> 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> { + return this.workflowItemService.findById(route.params.id, + followLink('item'), + ).pipe( + find((RD) => hasValue(RD.error) || RD.hasSucceeded), + ); + } +} diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts index d5df70698c..88402ec177 100644 --- a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts @@ -3,21 +3,64 @@ import { RouterModule } from '@angular/router'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { SubmissionEditComponent } from '../submission/edit/submission-edit.component'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { getWorkflowItemModulePath } from '../app-routing.module'; +import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; +import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; + +export function getWorkflowItemPageRoute(wfiId: string) { + return new URLCombiner(getWorkflowItemModulePath(), wfiId).toString(); +} + +export function getWorkflowItemEditPath(wfiId: string) { + return new URLCombiner(getWorkflowItemModulePath(), wfiId, WORKFLOW_ITEM_EDIT_PATH).toString() +} + +export function getWorkflowItemDeletePath(wfiId: string) { + return new URLCombiner(getWorkflowItemModulePath(), wfiId, WORKFLOW_ITEM_DELETE_PATH).toString() +} + +export function getWorkflowItemSendBackPath(wfiId: string) { + return new URLCombiner(getWorkflowItemModulePath(), wfiId, WORKFLOW_ITEM_SEND_BACK_PATH).toString() +} + +const WORKFLOW_ITEM_EDIT_PATH = 'edit'; +const WORKFLOW_ITEM_DELETE_PATH = 'delete'; +const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', redirectTo: '/home', pathMatch: 'full' }, { - canActivate: [AuthenticatedGuard], - path: ':id/edit', - component: SubmissionEditComponent, - data: { title: 'submission.edit.title' } - } - ]) - ] + path: ':id', + resolve: { wfi: WorkflowItemPageResolver }, + children: [ + { + canActivate: [AuthenticatedGuard], + path: WORKFLOW_ITEM_EDIT_PATH, + component: SubmissionEditComponent, + data: { title: 'submission.edit.title' } + }, + { + canActivate: [AuthenticatedGuard], + path: WORKFLOW_ITEM_DELETE_PATH, + component: WorkflowItemDeleteComponent, + data: { title: 'workflow-item.delete.title' } + }, + { + canActivate: [AuthenticatedGuard], + path: WORKFLOW_ITEM_SEND_BACK_PATH, + component: WorkflowItemDeleteComponent, + data: { title: 'workflow-item.sendback.title' } + } + ] + }] + ) + ], + providers: [WorkflowItemPageResolver] }) /** * This module defines the default component to load when navigating to the workflowitems edit page path. */ -export class WorkflowItemsEditPageRoutingModule { } +export class WorkflowItemsEditPageRoutingModule { +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 06811abd08..0ce8530bf2 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -41,6 +41,12 @@ export function getProfileModulePath() { return `/${PROFILE_MODULE_PATH}`; } +const WORKFLOW_ITEM_MODULE_PATH = 'workflowitems'; + +export function getWorkflowItemModulePath() { + return `/${WORKFLOW_ITEM_MODULE_PATH}`; +} + export function getDSOPath(dso: DSpaceObject): string { switch ((dso as any).type) { case Community.type.value: @@ -79,7 +85,7 @@ export function getDSOPath(dso: DSpaceObject): string { loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' }, { - path: 'workflowitems', + path: WORKFLOW_ITEM_MODULE_PATH, loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' }, { diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index a2dfca5eb3..370b2df6dd 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -9,10 +9,14 @@ import { DataService } from '../data/data.service'; import { RequestService } from '../data/request.service'; import { WorkflowItem } from './models/workflowitem.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindListOptions } from '../data/request.models'; +import { DeleteByIDRequest, FindListOptions } from '../data/request.models'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { Observable } from 'rxjs'; +import { find, map } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { RequestEntry } from '../data/request.reducer'; /** * A service that provides methods to make REST requests with workflowitems endpoint. @@ -35,4 +39,38 @@ export class WorkflowItemDataService extends DataService { super(); } + /** + * Delete an existing Workspace Item on the server + * @param id The Workspace Item's id to be removed + * @return an observable that emits true when the deletion was successful, false when it failed + */ + delete(id: string): Observable { + return this.deleteWFI(id, true) + } + + sendBack(id: string): Observable { + return this.deleteWFI(id, false) + } + + private deleteWFI(id: string, expunge: boolean): Observable { + const requestId = this.requestService.generateRequestId(); + + const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, id)), + map((endpoint: string) => endpoint + '?expunge=' + expunge) + ); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new DeleteByIDRequest(requestId, href, id); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response.isSuccessful) + ); + } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 698d9c936c..88c26792ad 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -186,6 +186,7 @@ import { LogInPasswordComponent } from './log-in/methods/password/log-in-passwor import { LogInComponent } from './log-in/log-in.component'; import { MissingTranslationHelper } from './translate/missing-translation.helper'; import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component'; +import { ModifyItemOverviewComponent } from '../+item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -357,7 +358,8 @@ const COMPONENTS = [ LogInContainerComponent, ItemVersionsComponent, PublicationSearchResultListElementComponent, - ItemVersionsNoticeComponent + ItemVersionsNoticeComponent, + ModifyItemOverviewComponent, ]; const ENTRY_COMPONENTS = [ From de1b57d8d951f263632b35dc5d59e9bc4819496d Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 3 Apr 2020 18:13:22 +0200 Subject: [PATCH 021/103] workflow delete/sendback --- resources/i18n/en.json5 | 43 +++++++++++- ...search-result-grid-element.component.html} | 1 + ...search-result-grid-element.component.scss} | 0 ...rch-result-grid-element.component.spec.ts} | 10 +-- ...w-search-result-grid-element.component.ts} | 38 +++++------ ...admin-workflow-grid-element.component.html | 6 +- ...m-admin-workflow-grid-element.component.ts | 50 +++++++++----- ...search-result-list-element.component.html} | 0 ...search-result-list-element.component.scss} | 0 ...rch-result-list-element.component.spec.ts} | 10 +-- ...w-search-result-list-element.component.ts} | 9 ++- ...item-admin-workflow-actions.component.html | 8 +-- src/app/+admin/admin.module.ts | 12 ++-- .../workflow-item-action-page.component.html | 6 ++ .../workflow-item-action-page.component.ts | 65 +++++++++++++++++++ .../workflow-item-delete.component.html | 6 -- .../workflow-item-delete.component.scss | 0 .../workflow-item-delete.component.ts | 63 +++++------------- .../workflow-item-send-back.component.spec.ts | 25 +++++++ .../workflow-item-send-back.component.ts | 31 +++++++++ .../workflowitems-edit-page-routing.module.ts | 5 +- .../workflowitems-edit-page.module.ts | 3 +- .../lookup/dynamic-lookup.component.spec.ts | 2 - .../publication-grid-element.component.html | 5 +- 24 files changed, 271 insertions(+), 127 deletions(-) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/{pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.html => task-search-result/task-admin-workflow-search-result-grid-element.component.html} (87%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/{pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.scss => task-search-result/task-admin-workflow-search-result-grid-element.component.scss} (100%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/{pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.spec.ts => task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts} (89%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/{pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts => task-search-result/task-admin-workflow-search-result-grid-element.component.ts} (52%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/{pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html => task-item-search-result/task-admin-workflow-search-result-list-element.component.html} (100%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/{pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.scss => task-item-search-result/task-admin-workflow-search-result-list-element.component.scss} (100%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/{pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.spec.ts => task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts} (86%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/{pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts => task-item-search-result/task-admin-workflow-search-result-list-element.component.ts} (83%) create mode 100644 src/app/+workflowitems-edit-page/workflow-item-action-page.component.html create mode 100644 src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts delete mode 100644 src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html delete mode 100644 src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.scss create mode 100644 src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts create mode 100644 src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index f874a02bdf..a515113445 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -267,6 +267,12 @@ "admin.workflow.title": "Admin Workflow", + "admin.workflow.item.workflow": "Workflow", + + "admin.workflow.item.delete": "Delete", + + "admin.workflow.item.send-back": "Send back", + @@ -2224,8 +2230,43 @@ "virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata", - "workflowAdmin.search.results.head": "Active Workflows", + "workflowAdmin.search.results.head": "Active Workflows", + + + + "workflow-item.delete.notification.success.title": "Deleted", + + "workflow-item.delete.notification.success.content": "This workflow item was successfully deleted", + + "workflow-item.delete.notification.error.title": "Something went wrong", + + "workflow-item.delete.notification.error.content": "The workflow item could not be deleted", + + "workflow-item.delete.title": "Delete workflow item", + + "workflow-item.delete.header": "Delete workflow item", + + "workflow-item.delete.button.cancel": "Cancel", + + "workflow-item.delete.button.confirm": "Delete", + + + "workflow-item.send-back.notification.success.title": "Sent back to submitter", + + "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", + + "workflow-item.send-back.notification.error.title": "Something went wrong", + + "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", + + "workflow-item.send-back.title": "Send workflow item back to submitter", + + "workflow-item.send-back.header": "Send workflow item back to submitter", + + "workflow-item.send-back.button.cancel": "Cancel", + + "workflow-item.send-back.button.confirm": "Send back", } diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.html similarity index 87% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.html rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.html index c73a5f5a02..8403e632db 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.html +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.html @@ -1,4 +1,5 @@ { - let component: PoolTaskAdminWorkflowSearchResultGridElementComponent; - let fixture: ComponentFixture; + let component: TaskAdminWorkflowSearchResultGridElementComponent; + let fixture: ComponentFixture; let id; let searchResult; @@ -42,7 +42,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { init(); TestBed.configureTestingModule( { - declarations: [PoolTaskAdminWorkflowSearchResultGridElementComponent], + declarations: [TaskAdminWorkflowSearchResultGridElementComponent], imports: [ NoopAnimationsModule, TranslateModule.forRoot(), @@ -59,7 +59,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(PoolTaskAdminWorkflowSearchResultGridElementComponent); + fixture = TestBed.createComponent(TaskAdminWorkflowSearchResultGridElementComponent); component = fixture.componentInstance; component.object = searchResult; component.linkTypes = CollectionElementLinkType; diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts similarity index 52% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts index 70659a3f03..1f394fe134 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts @@ -1,26 +1,9 @@ -import { Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; -import { Item } from '../../../../../core/shared/item.model'; +import { Component, OnInit } from '@angular/core'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { Context } from '../../../../../core/shared/context.model'; -import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; -import { getItemEditPath } from '../../../../../+item-page/item-page-routing.module'; -import { URLCombiner } from '../../../../../core/url-combiner/url-combiner'; -import { - ITEM_EDIT_DELETE_PATH, - ITEM_EDIT_MOVE_PATH, - ITEM_EDIT_PRIVATE_PATH, - ITEM_EDIT_PUBLIC_PATH, - ITEM_EDIT_REINSTATE_PATH, - ITEM_EDIT_WITHDRAW_PATH -} from '../../../../../+item-page/edit-item-page/edit-item-page.routing.module'; import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; -import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; -import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; -import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; -import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; import { PoolTaskSearchResult } from '../../../../../shared/object-collection/shared/pool-task-search-result.model'; -import { PoolTask } from '../../../../../core/tasks/models/pool-task-object.model'; import { Observable } from 'rxjs'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { RemoteData } from '../../../../../core/data/remote-data'; @@ -28,22 +11,31 @@ import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../ import { SearchResult } from '../../../../../shared/search/search-result.model'; import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; import { ClaimedTaskSearchResult } from '../../../../../shared/object-collection/shared/claimed-task-search-result.model'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; @listableObjectComponent(PoolTaskSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @listableObjectComponent(ClaimedTaskSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ - selector: 'ds-pool-task-admin-workflow-search-result-grid-element', - styleUrls: ['./pool-task-admin-workflow-search-result-grid-element.component.scss'], - templateUrl: './pool-task-admin-workflow-search-result-grid-element.component.html' + selector: 'ds-task-admin-workflow-search-result-grid-element', + styleUrls: ['./task-admin-workflow-search-result-grid-element.component.scss'], + templateUrl: './task-admin-workflow-search-result-grid-element.component.html' }) /** * The component for displaying a list element for an pool task search result on the admin search page */ -export class PoolTaskAdminWorkflowSearchResultGridElementComponent extends SearchResultGridElementComponent, TaskObject> implements OnInit { +export class TaskAdminWorkflowSearchResultGridElementComponent extends SearchResultGridElementComponent, TaskObject> implements OnInit { public wfi$: Observable; + constructor(private linkService: LinkService, protected truncatableService: TruncatableService, protected bitstreamService: BitstreamDataService) { + super(truncatableService, bitstreamService); + } + ngOnInit(): void { super.ngOnInit(); + this.dso = this.linkService.resolveLink(this.dso, followLink('workflowitem')); this.wfi$ = (this.dso.workflowitem as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); } } diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html index 6f940d097e..d6b76c0a8d 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html @@ -6,7 +6,7 @@
    -
  • - -
  • +
  • + +
diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts index f00dc4fdb4..2ee9f16f52 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts @@ -22,6 +22,12 @@ import { ListableObjectDirective } from '../../../../../shared/object-collection import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { Observable } from 'rxjs'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { take } from 'rxjs/operators'; @listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ @@ -36,8 +42,9 @@ export class WorkflowItemAdminWorkflowGridElementComponent extends AbstractLista @ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective; @ViewChild('badges', { static: true }) badges: ElementRef; @ViewChild('buttons', { static: true }) buttons: ElementRef; + public item$: Observable; - constructor(private componentFactoryResolver: ComponentFactoryResolver) { + constructor(private componentFactoryResolver: ComponentFactoryResolver, private linkService: LinkService) { super(); } @@ -45,30 +52,37 @@ export class WorkflowItemAdminWorkflowGridElementComponent extends AbstractLista * Setup the dynamic child component */ ngOnInit(): void { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent()); + this.object = this.linkService.resolveLink(this.object, followLink('item')); + this.item$ = (this.object.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); + this.item$.pipe(take(1)).subscribe((item: Item) => { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item)); - const viewContainerRef = this.listableObjectDirective.viewContainerRef; - viewContainerRef.clear(); + const viewContainerRef = this.listableObjectDirective.viewContainerRef; + viewContainerRef.clear(); - const componentRef = viewContainerRef.createComponent( - componentFactory, - 0, - undefined, - [ - [this.badges.nativeElement], - [this.buttons.nativeElement] - ]); - (componentRef.instance as any).object = this.object; - (componentRef.instance as any).index = this.index; - (componentRef.instance as any).linkType = this.linkType; - (componentRef.instance as any).listID = this.listID; + const componentRef = viewContainerRef.createComponent( + componentFactory, + 0, + undefined, + [ + [this.badges.nativeElement], + [this.buttons.nativeElement] + ]); + (componentRef.instance as any).object = item; + (componentRef.instance as any).index = this.index; + (componentRef.instance as any).linkType = this.linkType; + (componentRef.instance as any).listID = this.listID; + componentRef.changeDetectorRef.detectChanges(); + } + ) } /** * Fetch the component depending on the item's relationship type, view mode and context * @returns {GenericConstructor} */ - private getComponent(): GenericConstructor { - return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined) + private getComponent(item: Item): GenericConstructor { + return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined) } + } diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.html similarity index 100% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.html rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.html diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.scss similarity index 100% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.scss rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.scss diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts similarity index 86% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.spec.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts index 63f36ea033..684dd8f108 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts @@ -8,12 +8,12 @@ import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; -import { PoolTaskAdminWorkflowSearchResultListElementComponent } from './pool-task-admin-workflow-search-result-list-element.component'; +import { TaskAdminWorkflowSearchResultListElementComponent } from './task-admin-workflow-search-result-list-element.component'; import { Item } from '../../../../../core/shared/item.model'; describe('ItemAdminSearchResultListElementComponent', () => { - let component: PoolTaskAdminWorkflowSearchResultListElementComponent; - let fixture: ComponentFixture; + let component: TaskAdminWorkflowSearchResultListElementComponent; + let fixture: ComponentFixture; let id; let searchResult; @@ -31,7 +31,7 @@ describe('ItemAdminSearchResultListElementComponent', () => { TranslateModule.forRoot(), RouterTestingModule.withRoutes([]) ], - declarations: [PoolTaskAdminWorkflowSearchResultListElementComponent], + declarations: [TaskAdminWorkflowSearchResultListElementComponent], providers: [{ provide: TruncatableService, useValue: {} }], schemas: [NO_ERRORS_SCHEMA] }) @@ -39,7 +39,7 @@ describe('ItemAdminSearchResultListElementComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(PoolTaskAdminWorkflowSearchResultListElementComponent); + fixture = TestBed.createComponent(TaskAdminWorkflowSearchResultListElementComponent); component = fixture.componentInstance; component.object = searchResult; component.linkTypes = CollectionElementLinkType; diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts similarity index 83% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts index db23a3c4aa..2de768b747 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts @@ -14,19 +14,18 @@ import { SearchResult } from '../../../../../shared/search/search-result.model'; import { followLink } from '../../../../../shared/utils/follow-link-config.model'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; -import { tap } from 'rxjs/operators'; @listableObjectComponent(PoolTaskSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) @listableObjectComponent(ClaimedTaskSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) @Component({ - selector: 'ds-pooltask-admin-workflow-search-result-list-element', - styleUrls: ['./pool-task-admin-workflow-search-result-list-element.component.scss'], - templateUrl: './pool-task-admin-workflow-search-result-list-element.component.html' + selector: 'ds-task-admin-workflow-search-result-list-element', + styleUrls: ['./task-admin-workflow-search-result-list-element.component.scss'], + templateUrl: './task-admin-workflow-search-result-list-element.component.html' }) /** * The component for displaying a list element for an pool task search result on the admin search page */ -export class PoolTaskAdminWorkflowSearchResultListElementComponent extends SearchResultListElementComponent, TaskObject> { +export class TaskAdminWorkflowSearchResultListElementComponent extends SearchResultListElementComponent, TaskObject> { public wfi$: Observable; constructor(private linkService: LinkService, protected truncatableService: TruncatableService) { diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html index 3004eb925c..1a90a4cff4 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html @@ -1,7 +1,7 @@ - - {{"admin.search.item.delete" | translate}} + + {{"admin.workflow.item.delete" | translate}} - - {{"admin.search.item.send-back" | translate}} + + {{"admin.workflow.item.send-back" | translate}} diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts index b829c8d868..cd10aad1ca 100644 --- a/src/app/+admin/admin.module.ts +++ b/src/app/+admin/admin.module.ts @@ -16,8 +16,8 @@ import { WorkflowItemAdminWorkflowGridElementComponent } from './admin-workflow- import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component'; import { WorkflowItemAdminWorkflowListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; -import { PoolTaskAdminWorkflowSearchResultListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/pool-task-item-search-result/pool-task-admin-workflow-search-result-list-element.component'; -import { PoolTaskAdminWorkflowSearchResultGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/pool-task-search-result/pool-task-admin-workflow-search-result-grid-element.component'; +import { TaskAdminWorkflowSearchResultGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component'; +import { TaskAdminWorkflowSearchResultListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component'; @NgModule({ imports: [ @@ -42,8 +42,8 @@ import { PoolTaskAdminWorkflowSearchResultGridElementComponent } from './admin-w WorkflowItemAdminWorkflowGridElementComponent, WorkflowItemAdminWorkflowActionsComponent, - PoolTaskAdminWorkflowSearchResultListElementComponent, - PoolTaskAdminWorkflowSearchResultGridElementComponent, + TaskAdminWorkflowSearchResultGridElementComponent, + TaskAdminWorkflowSearchResultListElementComponent, ], entryComponents: [ ItemAdminSearchResultListElementComponent, @@ -58,8 +58,8 @@ import { PoolTaskAdminWorkflowSearchResultGridElementComponent } from './admin-w WorkflowItemAdminWorkflowGridElementComponent, WorkflowItemAdminWorkflowActionsComponent, - PoolTaskAdminWorkflowSearchResultListElementComponent, - PoolTaskAdminWorkflowSearchResultGridElementComponent, + TaskAdminWorkflowSearchResultGridElementComponent, + TaskAdminWorkflowSearchResultListElementComponent, ] }) export class AdminModule { diff --git a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.html b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.html new file mode 100644 index 0000000000..76808c7e14 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.html @@ -0,0 +1,6 @@ +
+

{{'workflow-item.' + type + '.header' | translate}}

+ + + +
diff --git a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts new file mode 100644 index 0000000000..fcc943a741 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts @@ -0,0 +1,65 @@ +import { OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, switchMap, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { Item } from '../core/shared/item.model'; +import { ActivatedRoute, Data, Router } from '@angular/router'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; +import { RouteService } from '../core/services/route.service'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { RemoteData } from '../core/data/remote-data'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators'; +import { isEmpty } from '../shared/empty.util'; + +export abstract class WorkflowItemActionPageComponent implements OnInit { + public type; + public wfi$: Observable; + public item$: Observable; + + constructor(protected route: ActivatedRoute, + protected workflowItemService: WorkflowItemDataService, + protected router: Router, + protected routeService: RouteService, + protected notificationsService: NotificationsService, + protected translationService: TranslateService) { + } + + ngOnInit() { + this.type = this.getType(); + this.wfi$ = this.route.data.pipe(map((data: Data) => data.wfi as RemoteData), getRemoteDataPayload()); + this.item$ = this.wfi$.pipe(switchMap((wfi: WorkflowItem) => (wfi.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + } + + performAction() { + this.wfi$.pipe( + take(1), + switchMap((wfi: WorkflowItem) => this.sendRequest(wfi.id)) + ).subscribe((successful: boolean) => { + if (successful) { + const title = this.translationService.get('workflow-item.' + this.type + '.notification.success.title'); + const content = this.translationService.get('workflow-item.' + this.type + '.notification.success.content'); + this.notificationsService.success(title, content) + } else { + const title = this.translationService.get('workflow-item.' + this.type + '.notification.error.title'); + const content = this.translationService.get('workflow-item.' + this.type + '.notification.error.content'); + this.notificationsService.error(title, content) + } + this.previousPage(); + }) + } + + previousPage() { + this.routeService.getPreviousUrl().pipe(take(1)) + .subscribe((url) => { + if (isEmpty(url)) { + url = '/mydspace'; + } + this.router.navigateByUrl(url); + + } + ); + } + abstract sendRequest(id: string): Observable; + abstract getType(): string; +} diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html deleted file mode 100644 index 5b1b8c45b3..0000000000 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.html +++ /dev/null @@ -1,6 +0,0 @@ -
- - - - -
diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.scss b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts index 4c6f0ff885..171aeb27d1 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts @@ -1,58 +1,31 @@ -import { Component, OnInit } from '@angular/core'; -import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; -import { Item } from '../../core/shared/item.model'; +import { Component } from '@angular/core'; import { Observable } from 'rxjs'; -import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../core/shared/operators'; -import { RemoteData } from '../../core/data/remote-data'; -import { ActivatedRoute, Data, Router } from '@angular/router'; -import { map, switchMap, take } from 'rxjs/operators'; +import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component'; +import { ActivatedRoute, Router } from '@angular/router'; import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; -import { TranslateService } from '@ngx-translate/core'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { RouteService } from '../../core/services/route.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-workflow-item-delete', - templateUrl: './workflow-item-delete.component.html', - styleUrls: ['./workflow-item-delete.component.scss'] + templateUrl: '../workflow-item-action-page.component.html' }) -export class WorkflowItemDeleteComponent implements OnInit { - public wfi$: Observable; - public item$: Observable; - - constructor(private route: ActivatedRoute, - private workflowItemService: WorkflowItemDataService, - private router: Router, - private routeService: RouteService, - private notificationsService: NotificationsService, - private translationService: TranslateService) { +export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent { + constructor(protected route: ActivatedRoute, + protected workflowItemService: WorkflowItemDataService, + protected router: Router, + protected routeService: RouteService, + protected notificationsService: NotificationsService, + protected translationService: TranslateService) { + super(route, workflowItemService, router, routeService, notificationsService, translationService); } - ngOnInit() { - this.route.data.subscribe((t) => console.log(t)); - this.wfi$ = this.route.data.pipe(map((data: Data) => data.wfi as RemoteData), getRemoteDataPayload()); - this.item$ = this.wfi$.pipe(switchMap((wfi: WorkflowItem) => (wfi.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + getType(): string { + return 'delete'; } - delete() { - this.wfi$.pipe( - take(1), - switchMap((wfi: WorkflowItem) => this.workflowItemService.delete(wfi.id)) - ).subscribe((successful: boolean) => { - if (successful) { - const title = this.translationService.get('workflowitem.delete.notification.success.title'); - const content = this.translationService.get('workflowitem.delete.notification.success.content'); - this.notificationsService.success(title, content) - } else { - const title = this.translationService.get('workflowitem.delete.notification.error.title'); - const content = this.translationService.get('workflowitem.delete.notification.error.content'); - this.notificationsService.error(title, content) - } - this.previousPage(); - }) - } - - previousPage() { - this.routeService.getPreviousUrl(); + sendRequest(id: string): Observable { + return this.workflowItemService.delete(id); } } diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts new file mode 100644 index 0000000000..ba293845d9 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WorkflowItemDeleteComponent } from './workflow-item-send-back.component'; + +describe('WorkflowItemDeleteComponent', () => { + let component: WorkflowItemDeleteComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ WorkflowItemDeleteComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WorkflowItemDeleteComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts new file mode 100644 index 0000000000..c4006811d9 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component'; +import { Observable } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { RouteService } from '../../core/services/route.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'ds-workflow-item-send-back', + templateUrl: '../workflow-item-action-page.component.html' +}) +export class WorkflowItemSendBackComponent extends WorkflowItemActionPageComponent { + constructor(protected route: ActivatedRoute, + protected workflowItemService: WorkflowItemDataService, + protected router: Router, + protected routeService: RouteService, + protected notificationsService: NotificationsService, + protected translationService: TranslateService) { + super(route, workflowItemService, router, routeService, notificationsService, translationService); + } + + getType(): string { + return 'send-back'; + } + + sendRequest(id: string): Observable { + return this.workflowItemService.sendBack(id); + } +} diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts index 88402ec177..e9989bf947 100644 --- a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts @@ -7,6 +7,7 @@ import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getWorkflowItemModulePath } from '../app-routing.module'; import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; +import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component'; export function getWorkflowItemPageRoute(wfiId: string) { return new URLCombiner(getWorkflowItemModulePath(), wfiId).toString(); @@ -50,8 +51,8 @@ const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback'; { canActivate: [AuthenticatedGuard], path: WORKFLOW_ITEM_SEND_BACK_PATH, - component: WorkflowItemDeleteComponent, - data: { title: 'workflow-item.sendback.title' } + component: WorkflowItemSendBackComponent, + data: { title: 'workflow-item.send-back.title' } } ] }] diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts index 8e301390ae..ef1e49abf5 100644 --- a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts @@ -4,6 +4,7 @@ import { SharedModule } from '../shared/shared.module'; import { WorkflowItemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module'; import { SubmissionModule } from '../submission/submission.module'; import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; +import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component'; @NgModule({ imports: [ @@ -12,7 +13,7 @@ import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-ite SharedModule, SubmissionModule, ], - declarations: [WorkflowItemDeleteComponent] + declarations: [WorkflowItemDeleteComponent, WorkflowItemSendBackComponent] }) /** * This module handles all modules that need to access the workflowitems edit page. diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index c1f8ad69ba..c249c797a6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -27,8 +27,6 @@ import { AuthorityConfidenceStateDirective } from '../../../../../authority-conf import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../../../../../config'; import { MOCK_SUBMISSION_CONFIG } from '../../../../../testing/mock-submission-config'; -import { WorkspaceitemsEditPageModule } from '../../../../../../+workspaceitems-edit-page/workspaceitems-edit-page.module'; -import { WorkspaceItem } from '../../../../../../core/submission/models/workspaceitem.model'; let LOOKUP_TEST_MODEL_CONFIG = { authorityOptions: { diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html index 81ee3ebcce..a9b5d0bdaa 100644 --- a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html @@ -1 +1,4 @@ - + + + + From 6314e17f274b20d9131d8e5ac9c9bfc0d1d82be7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 3 Apr 2020 20:25:58 +0200 Subject: [PATCH 022/103] Intermediate commit --- resources/i18n/en.json5 | 20 +++- .../edit-item-page/edit-item-page.module.ts | 6 +- .../edit-item-page.routing.module.ts | 25 +++-- .../item-authorizations.component.html | 6 +- .../resource-policy.service.spec.ts | 21 ++--- .../resource-policy.service.ts | 9 ++ src/app/core/shared/operators.ts | 21 +++++ .../resource-policy-create.component.html | 4 +- .../resource-policy-create.component.ts | 69 ++++++++++---- .../edit/resource-policy-edit.component.html | 6 +- .../edit/resource-policy-edit.component.ts | 55 ++++++++++- .../eperson-group-list.component.ts | 19 ++-- .../form/resource-policy-form.html | 2 +- .../form/resource-policy-form.ts | 92 ++++++++++++++----- .../resource-policy-target.resolver.ts | 53 +++++++++++ .../resolvers/resource-policy.resolver.ts | 40 ++++++++ .../resource-policies.component.html | 6 +- .../resource-policies.component.ts | 81 ++++++++++++---- src/app/shared/shared.module.ts | 14 ++- 19 files changed, 443 insertions(+), 106 deletions(-) create mode 100644 src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts create mode 100644 src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 7664b8967a..4fee874bb3 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1675,9 +1675,21 @@ "resource-policies.add.for.item": "Add a new Item policy", - "resource-policies.create.modal.head": "Create new resource policy", + "resource-policies.create.page.heading": "Create new resource policy for ", - "resource-policies.edit.modal.head": "Edit resource policy", + "resource-policies.create.page.failure.content": "An error occurred while creating the resource policy.", + + "resource-policies.create.page.success.content": "Operation successful", + + "resource-policies.create.page.title": "Create new resource policy", + + "resource-policies.edit.page.heading": "Edit resource policy ", + + "resource-policies.edit.page.failure.content": "An error occurred while editing the resource policy.", + + "resource-policies.edit.page.success.content": "Operation successful", + + "resource-policies.edit.page.title": "Edit resource policy", "resource-policies.form.action-type.label": "Select the action type", @@ -1721,9 +1733,11 @@ "resource-policies.table.headers.group": "Group", + "resource-policies.table.headers.id": "ID", + "resource-policies.table.headers.name": "Name", - "resource-policies.table.headers.id": "ID", + "resource-policies.table.headers.policyType": "type", "resource-policies.table.headers.title.for.bitstream": "Policies for Bitstream", diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index 2b1248e61f..13507c31ce 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -24,6 +24,8 @@ import { ItemMoveComponent } from './item-move/item-move.component'; import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; +import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; +import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -56,7 +58,9 @@ import { ItemAuthorizationsComponent } from './item-authorizations/item-authoriz ItemCollectionMapperComponent, ItemMoveComponent, VirtualMetadataComponent, - ItemAuthorizationsComponent + ItemAuthorizationsComponent, + ResourcePolicyEditComponent, + ResourcePolicyCreateComponent, ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index c9bb14b1a9..aa42d8ed24 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -15,8 +15,10 @@ import { ItemRelationshipsComponent } from './item-relationships/item-relationsh import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; -import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; +import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; +import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; +import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; export const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; @@ -118,19 +120,27 @@ export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations'; }, { path: ITEM_EDIT_AUTHORIZATIONS_PATH, - data: { title: 'item.edit.authorizations.title' }, children: [ { - path: ':dso/create', + path: 'create', + resolve: { + resourcePolicyTarget: ResourcePolicyTargetResolver + }, component: ResourcePolicyCreateComponent, + data: { title: 'resource-policies.create.page.title' } }, { - path: ':dso/:policy/edit', + path: 'edit', + resolve: { + resourcePolicy: ResourcePolicyResolver + }, component: ResourcePolicyEditComponent, + data: { title: 'resource-policies.edit.page.title' } }, { path: '', - component: ItemAuthorizationsComponent + component: ItemAuthorizationsComponent, + data: { title: 'item.edit.authorizations.title' } } ] } @@ -138,7 +148,10 @@ export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations'; } ]) ], - providers: [] + providers: [ + ResourcePolicyResolver, + ResourcePolicyTargetResolver + ] }) export class EditItemPageRoutingModule { diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html index cb22d93868..0cf61579f1 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html @@ -1,11 +1,11 @@
- + - - diff --git a/src/app/core/resource-policy/resource-policy.service.spec.ts b/src/app/core/resource-policy/resource-policy.service.spec.ts index 6c26973e02..78566d61a3 100644 --- a/src/app/core/resource-policy/resource-policy.service.spec.ts +++ b/src/app/core/resource-policy/resource-policy.service.spec.ts @@ -170,18 +170,6 @@ describe('ResourcePolicyService', () => { }); describe('delete', () => { - beforeEach(async(() => { - scheduler = getTestScheduler(); - responseCacheEntry.completed = true; - requestService = jasmine.createSpyObj('requestService', { - configure: {}, - getByHref: observableOf(responseCacheEntry), - getByUUID: hot('a', { a: responseCacheEntry }), - generateRequestId: 'request-id', - removeByHrefSubstring: {} - }); - })); - it('should proxy the call to dataservice.create', () => { scheduler.schedule(() => service.delete(resourcePolicyId)); scheduler.flush(); @@ -190,6 +178,15 @@ describe('ResourcePolicyService', () => { }); }); + describe('update', () => { + it('should proxy the call to dataservice.update', () => { + scheduler.schedule(() => service.update(resourcePolicy)); + scheduler.flush(); + + expect((service as any).dataService.update).toHaveBeenCalledWith(resourcePolicy); + }); + }); + describe('findById', () => { it('should proxy the call to dataservice.findById', () => { scheduler.schedule(() => service.findById(resourcePolicyId)); diff --git a/src/app/core/resource-policy/resource-policy.service.ts b/src/app/core/resource-policy/resource-policy.service.ts index 44938ec6a3..291920c35a 100644 --- a/src/app/core/resource-policy/resource-policy.service.ts +++ b/src/app/core/resource-policy/resource-policy.service.ts @@ -103,6 +103,15 @@ export class ResourcePolicyService { return this.dataService.delete(resourcePolicyID); } + /** + * Add a new patch to the object cache + * The patch is derived from the differences between the given object and its version in the object cache + * @param {ResourcePolicy} object The given object + */ + update(object: ResourcePolicy): Observable> { + return this.dataService.update(object); + } + /** * Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on an href, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the {@link ResourcePolicy} diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 14d101a448..715e59df1a 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -67,6 +67,10 @@ export const getSucceededRemoteData = () => (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded)); +export const getSucceededRemoteWithNotEmptyData = () => + (source: Observable>): Observable> => + source.pipe(find((rd: RemoteData) => rd.hasSucceeded && isNotEmpty(rd.payload))); + /** * Get the first successful remotely retrieved object * @@ -84,6 +88,23 @@ export const getFirstSucceededRemoteDataPayload = () => getRemoteDataPayload() ); +/** + * Get the first successful remotely retrieved object with not empty payload + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getFirstSucceededRemoteDataWithNotEmptyPayload = () => + (source: Observable>): Observable => + source.pipe( + getSucceededRemoteWithNotEmptyData(), + getRemoteDataPayload() + ); + /** * Get the all successful remotely retrieved objects * diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.html b/src/app/shared/resource-policies/create/resource-policy-create.component.html index 7990b8eb43..c4eb42bb18 100644 --- a/src/app/shared/resource-policies/create/resource-policy-create.component.html +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.html @@ -1,6 +1,6 @@
-

{{'resource-policies.create.modal.head' | translate}}

+

{{'resource-policies.edit.page.heading' | translate}} {{targetResourceName}}

-
diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.ts b/src/app/shared/resource-policies/create/resource-policy-create.component.ts index e92dab14da..375966509b 100644 --- a/src/app/shared/resource-policies/create/resource-policy-create.component.ts +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.ts @@ -1,32 +1,69 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; -import { take } from 'rxjs/operators'; +import { first, map, take } from 'rxjs/operators'; -import { ResourcePolicyEvent } from '../form/resource-policy-form'; -import { RouteService } from '../../../core/services/route.service'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; +import { ResourcePolicyEvent } from '../form/resource-policy-form'; +import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-resource-policy-create', templateUrl: './resource-policy-create.component.html' }) -export class ResourcePolicyCreateComponent { +export class ResourcePolicyCreateComponent implements OnInit { + + /** + * The uuid of the resource target of the policy + */ + private targetResourceUUID: string; + + public targetResourceName: string; constructor( - protected resourcePolicy: ResourcePolicyService, - protected router: Router, - protected routeService: RouteService) { + private dsoNameService: DSONameService, + private notificationsService: NotificationsService, + private resourcePolicyService: ResourcePolicyService, + private route: ActivatedRoute, + private router: Router) { + } + + ngOnInit(): void { + this.route.data.pipe( + map((data) => data), + take(1) + ).subscribe((data: any) => { + this.targetResourceUUID = (data.resourcePolicyTarget as RemoteData).payload.id; + this.targetResourceName = this.dsoNameService.getName((data.resourcePolicyTarget as RemoteData).payload); + }); + } + + redirectToAuthorizationsPage() { + this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route }); } createResourcePolicy(event: ResourcePolicyEvent) { - + let response$; + if (event.target.type === 'eperson') { + response$ = this.resourcePolicyService.create(event.object, this.targetResourceUUID, event.target.uuid); + } else { + response$ = this.resourcePolicyService.create(event.object, this.targetResourceUUID, null, event.target.uuid); + } + response$.pipe( + first((response: RemoteData) => !response.isResponsePending) + ).subscribe((responseRD: RemoteData) => { + if (responseRD.hasSucceeded) { + this.notificationsService.success(null, 'resource-policies.create.page.success.content'); + this.redirectToAuthorizationsPage(); + } else { + this.notificationsService.error(null, 'resource-policies.create.page.failure.content'); + } + }) } - redirectToPreviousPage() { - this.routeService.getPreviousUrl().pipe(take(1)) - .subscribe((url) => { - this.router.navigateByUrl(url); - }) - } } diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.html b/src/app/shared/resource-policies/edit/resource-policy-edit.component.html index d463e7fc1a..ede5519c74 100644 --- a/src/app/shared/resource-policies/edit/resource-policy-edit.component.html +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.html @@ -1,5 +1,7 @@
-

{{'resource-policies.edit.modal.head' | translate}}

+

{{'resource-policies.edit.page.heading' | translate}} {{resourcePolicy.id}}

- +
diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts index fa55979d35..844ac5b4e4 100644 --- a/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts @@ -1,9 +1,60 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { first, map, take } from 'rxjs/operators'; + +import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; +import { ResourcePolicyEvent } from '../form/resource-policy-form'; +import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; @Component({ selector: 'ds-resource-policy-edit', templateUrl: './resource-policy-edit.component.html' }) -export class ResourcePolicyEditComponent { +export class ResourcePolicyEditComponent implements OnInit { + /** + * The resource policy object to edit + */ + public resourcePolicy: ResourcePolicy; + + constructor( + private notificationsService: NotificationsService, + private resourcePolicyService: ResourcePolicyService, + private route: ActivatedRoute, + private router: Router) { + } + + ngOnInit(): void { + this.route.data.pipe( + map((data) => data), + take(1) + ).subscribe((data: any) => { + this.resourcePolicy = (data.resourcePolicy as RemoteData).payload; + console.log(data) + }); + } + + redirectToAuthorizationsPage() { + this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route }); + } + + updateResourcePolicy(event: ResourcePolicyEvent) { + const updatedObject = Object.assign({}, event.object, { + _links: this.resourcePolicy._links + }); + this.resourcePolicyService.update(updatedObject).pipe( + first((response: RemoteData) => !response.isResponsePending) + ).subscribe((responseRD: RemoteData) => { + if (responseRD.hasSucceeded) { + this.notificationsService.success(null, 'resource-policies.edit.page.success.content'); + this.redirectToAuthorizationsPage(); + } else { + this.notificationsService.error(null, 'resource-policies.edit.page.failure.content'); + } + }) + } } diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts index ed2f420438..2b4572cba5 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { map, take } from 'rxjs/operators'; @@ -7,13 +7,15 @@ import { uniqueId } from 'lodash' import { RemoteData } from '../../../../core/data/remote-data'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; -import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; -import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model'; import { DataService } from '../../../../core/data/data.service'; import { hasValue, isNotEmpty } from '../../../empty.util'; import { FindListOptions } from '../../../../core/data/request.models'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { getDataServiceFor } from '../../../../core/cache/builders/build-decorators'; +import { EPERSON } from '../../../../core/eperson/models/eperson.resource-type'; +import { GROUP } from '../../../../core/eperson/models/group.resource-type'; +import { ResourceType } from '../../../../core/shared/resource-type'; @Component({ selector: 'ds-eperson-group-list', @@ -69,9 +71,13 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { */ private subs: Subscription[] = []; - constructor(public dsoNameService: DSONameService, - private epersonService: EPersonDataService, - private groupsService: GroupDataService) { + constructor(public dsoNameService: DSONameService, private parentInjector: Injector) { + const resourceType: ResourceType = (this.isListOfEPerson) ? EPERSON : GROUP; + const provider = getDataServiceFor(resourceType); + this.dataService = Injector.create({ + providers: [], + parent: this.parentInjector + }).get(provider); } /** @@ -80,7 +86,6 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { ngOnInit(): void { this.paginationOptions.id = uniqueId('eperson-group-list-pagination'); this.paginationOptions.pageSize = 5; - this.dataService = (this.isListOfEPerson) ? this.epersonService : this.groupsService; if (this.initSelected) { this.entrySelectedId.next(this.initSelected); diff --git a/src/app/shared/resource-policies/form/resource-policy-form.html b/src/app/shared/resource-policies/form/resource-policy-form.html index 62b7ead932..999e7cf66c 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.html +++ b/src/app/shared/resource-policies/form/resource-policy-form.html @@ -7,7 +7,7 @@
- + diff --git a/src/app/shared/resource-policies/form/resource-policy-form.ts b/src/app/shared/resource-policies/form/resource-policy-form.ts index 9c08e8dcb3..e5b29cdaf1 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.ts @@ -1,6 +1,6 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { Observable } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; @@ -22,8 +22,11 @@ import { DsDynamicTextAreaModel } from '../../form/builder/ds-dynamic-form-ui/mo import { DynamicDsDatePickerModel } from '../../form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { hasValue, isNotEmpty } from '../../empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../empty.util'; import { FormService } from '../../form/form.service'; +import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Subscription } from 'rxjs/internal/Subscription'; export interface ResourcePolicyEvent { object: ResourcePolicy, @@ -40,10 +43,10 @@ export interface ResourcePolicyEvent { /** * Component that show form for adding/editing a resource policy */ -export class ResourcePolicyFormComponent implements OnInit { +export class ResourcePolicyFormComponent implements OnInit, OnDestroy { /** - * The resource policy to edit + * If given contains the resource policy to edit * @type {ResourcePolicy} */ @Input() resourcePolicy: ResourcePolicy; @@ -76,13 +79,19 @@ export class ResourcePolicyFormComponent implements OnInit { * The eperson or group that will be grant of the permission * @type {DSpaceObject} */ - public resourcePolicyTarget: DSpaceObject; + public resourcePolicyGrant: DSpaceObject; /** * The type of the object that will be grant of the permission. It could be 'eperson' or 'group' * @type {string} */ - public resourcePolicyTargetType: string; + public resourcePolicyGrantType: string; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + private subs: Subscription[] = []; /** * Initialize instance variables @@ -102,6 +111,14 @@ export class ResourcePolicyFormComponent implements OnInit { ngOnInit(): void { this.formId = this.formService.getUniqueId('resource-policy-form'); this.formModel = this.buildResourcePolicyForm(); + + if (!this.canSetGrant()) { + this.subs.push(observableCombineLatest([this.resourcePolicy.eperson, this.resourcePolicy.group]) + .subscribe(([epersonRD, groupRD]: [RemoteData, RemoteData]) => { + this.resourcePolicyGrant = epersonRD.payload || groupRD.payload; + }) + ) + } } /** @@ -111,7 +128,7 @@ export class ResourcePolicyFormComponent implements OnInit { */ isFormValid(): Observable { return this.formService.isValid(this.formId).pipe( - map((isValid: boolean) => isValid && isNotEmpty(this.resourcePolicyTarget)) + map((isValid: boolean) => isValid && isNotEmpty(this.resourcePolicyGrant)) ) } @@ -171,21 +188,31 @@ export class ResourcePolicyFormComponent implements OnInit { return formModel; } + /** + * Return a boolean representing If is possible to set policy grant + * + * @return true if is possible, false otherwise + */ + canSetGrant(): boolean { + return isEmpty(this.resourcePolicy); + } + /** * Return the name of the eperson or group that will be grant of the permission * * @return the object name */ getResourcePolicyTargetName(): string { - return isNotEmpty(this.resourcePolicyTarget) ? this.dsoNameService.getName(this.resourcePolicyTarget) : ''; + console.log(this.resourcePolicy); + return isNotEmpty(this.resourcePolicyGrant) ? this.dsoNameService.getName(this.resourcePolicyGrant) : ''; } /** * Update reference to the eperson or group that will be grant of the permission */ updateObjectSelected(object: DSpaceObject, isEPerson: boolean): void { - this.resourcePolicyTarget = object; - this.resourcePolicyTargetType = isEPerson ? 'eperson' : 'group'; + this.resourcePolicyGrant = object; + this.resourcePolicyGrantType = isEPerson ? 'eperson' : 'group'; } /** @@ -204,17 +231,40 @@ export class ResourcePolicyFormComponent implements OnInit { this.formService.getFormData(this.formId) .subscribe((data) => { const eventPayload: ResourcePolicyEvent = Object.create({}); - const resourcePolicy = new ResourcePolicy(); - resourcePolicy.name = data.name; - resourcePolicy.description = data.description; - resourcePolicy.policyType = data.policyType; - resourcePolicy.action = data.action; - resourcePolicy.startDate = (data.date) ? data.date.start : undefined; - resourcePolicy.endDate = (data.date) ? data.date.end : undefined; - eventPayload.object = resourcePolicy; - eventPayload.target.type = this.resourcePolicyTargetType; - eventPayload.target.uuid = this.resourcePolicyTarget.id; + eventPayload.object = this.createResourcePolicyByFormData(data); + console.log('resourcePolicyTarget', this.resourcePolicyGrant.type.value); + eventPayload.target = { + type: this.resourcePolicyGrantType, + uuid: this.resourcePolicyGrant.id + }; this.submit.emit(eventPayload); }) } + + /** + * Create e new ResourcePolicy by form data + * + * @return the new ResourcePolicy object + */ + createResourcePolicyByFormData(data): ResourcePolicy { + const resourcePolicy = new ResourcePolicy(); + resourcePolicy.name = (data.name) ? data.name[0].value : null; + resourcePolicy.description = (data.description) ? data.description[0].value : null; + resourcePolicy.policyType = (data.policyType) ? data.policyType[0].value : null; + resourcePolicy.action = (data.action) ? data.action[0].value : null; + resourcePolicy.startDate = (data.date && data.date.start) ? data.date.start[0].value : null; + resourcePolicy.endDate = (data.date && data.date.end) ? data.date.end[0].value : null; + resourcePolicy.type = RESOURCE_POLICY; + + return resourcePolicy; + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()) + } } diff --git a/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts b/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts new file mode 100644 index 0000000000..3580e86080 --- /dev/null +++ b/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts @@ -0,0 +1,53 @@ +import { Injectable, Injector } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { find } from 'rxjs/operators'; + +import { getDataServiceFor } from '../../../core/cache/builders/build-decorators'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { DataService } from '../../../core/data/data.service'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { hasValue, isEmpty } from '../../empty.util'; +import { RemoteData } from '../../../core/data/remote-data'; + +/** + * This class represents a resolver that requests a specific item before the route is activated + */ +@Injectable() +export class ResourcePolicyTargetResolver implements Resolve> { + + /** + * The data service used to make request. + */ + private dataService: DataService; + + constructor(private parentInjector: Injector, private router: Router) { + } + + /** + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> 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> { + const targetType = route.queryParamMap.get('targetType'); + const policyTargetId = route.queryParamMap.get('policyTargetId'); + + if (isEmpty(targetType) || isEmpty(policyTargetId)) { + this.router.navigateByUrl('/404', { skipLocationChange: true }); + } + + const provider = getDataServiceFor(new ResourceType(targetType)); + this.dataService = Injector.create({ + providers: [], + parent: this.parentInjector + }).get(provider); + + return this.dataService.findById(policyTargetId).pipe( + find((RD) => hasValue(RD.error) || RD.hasSucceeded), + ); + } +} diff --git a/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts b/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts new file mode 100644 index 0000000000..2e38aca5b6 --- /dev/null +++ b/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { find } from 'rxjs/operators'; + +import { hasValue, isEmpty } from '../../empty.util'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; +import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; +import { followLink } from '../../utils/follow-link-config.model'; + +/** + * This class represents a resolver that requests a specific item before the route is activated + */ +@Injectable() +export class ResourcePolicyResolver implements Resolve> { + + constructor(private resourcePolicyService: ResourcePolicyService, private router: Router) { + } + + /** + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> 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> { + const policyId = route.queryParamMap.get('policyId'); + + if (isEmpty(policyId)) { + this.router.navigateByUrl('/404', { skipLocationChange: true }); + } + + return this.resourcePolicyService.findById(policyId, followLink('eperson'), followLink('group')).pipe( + find((RD) => hasValue(RD.error) || RD.hasSucceeded), + ); + } +} diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index ecf810fbf8..19303b67ed 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -4,9 +4,9 @@
@@ -14,6 +14,7 @@ + @@ -30,6 +31,7 @@ +
+

{{ 'resource-policies.table.headers.title.for.' + resourceKey | translate }} {{resourceUUID}} -

@@ -15,6 +15,7 @@
{{'resource-policies.table.headers.id' | translate}} {{'resource-policies.table.headers.name' | translate}} {{'resource-policies.table.headers.action' | translate}}{{'resource-policies.table.headers.eperson' | translate}} {{'resource-policies.table.headers.group' | translate}} {{'resource-policies.table.headers.date.start' | translate}} {{'resource-policies.table.headers.date.end' | translate}}
{{policy.id}} + {{policy.id}} + + {{policy.name}} {{policy.action}} + {{getEPersonName(policy) | async}} + {{getGroupName(policy) | async}}

- {{ 'resource-policies.table.headers.title.for.' + resourceKey | translate }} {{resourceUUID}} + {{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} {{resourceUUID}}

{{'resource-policies.table.headers.id' | translate}} {{'resource-policies.table.headers.name' | translate}}{{'resource-policies.table.headers.policyType' | translate}} {{'resource-policies.table.headers.action' | translate}} {{'resource-policies.table.headers.eperson' | translate}} {{'resource-policies.table.headers.group' | translate}} {{policy.name}}{{policy.policyType}} {{policy.action}} {{getEPersonName(policy) | async}} diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts index 1f33437bfd..257657afdd 100644 --- a/src/app/shared/resource-policies/resource-policies.component.ts +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -1,12 +1,16 @@ import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { Observable, Subscription } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { filter, map, startWith, take } from 'rxjs/operators'; import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service'; import { PaginatedList } from '../../core/data/paginated-list'; -import { getFirstSucceededRemoteDataPayload, getSucceededRemoteData } from '../../core/shared/operators'; +import { + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteDataWithNotEmptyPayload, + getSucceededRemoteData +} from '../../core/shared/operators'; import { RemoteData } from '../../core/data/remote-data'; import { ResourcePolicy } from '../../core/resource-policy/models/resource-policy.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @@ -15,6 +19,8 @@ import { GroupDataService } from '../../core/eperson/group-data.service'; import { hasValue, isNotEmpty } from '../empty.util'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { followLink } from '../utils/follow-link-config.model'; +import { RequestService } from '../../core/data/request.service'; @Component({ selector: 'ds-resource-policies', @@ -36,13 +42,20 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * The resource type (e.g. 'item', 'bundle' etc) used as key to build automatically translation label * @type {string} */ - @Input() public resourceKey: string; + @Input() public resourceType: string; + + /** + * A boolean representing if component is active + * @type {boolean} + */ + private isActive: boolean; /** * The list of policies for given resource * @type {Observable>>} */ - private resourcePolicies$: Observable>>; + private resourcePolicies$: BehaviorSubject>> = + new BehaviorSubject>>({} as any); /** * Array to track all subscriptions and unsubscribe them onDestroy @@ -57,6 +70,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * @param {DSONameService} dsoNameService * @param {EPersonDataService} ePersonService * @param {GroupDataService} groupService + * @param {RequestService} requestService * @param {ResourcePolicyService} resourcePolicyService * @param {ActivatedRoute} route * @param {Router} router @@ -66,6 +80,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { private dsoNameService: DSONameService, private ePersonService: EPersonDataService, private groupService: GroupDataService, + private requestService: RequestService, private resourcePolicyService: ResourcePolicyService, private route: ActivatedRoute, private router: Router @@ -76,9 +91,16 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * Initialize the component, setting up the resource's policies */ ngOnInit(): void { - this.resourcePolicies$ = this.resourcePolicyService.searchByResource(this.resourceUUID).pipe( - getSucceededRemoteData() - ); + this.isActive = true; + this.requestService.removeByHrefSubstring(this.resourceUUID); + this.resourcePolicyService.searchByResource(this.resourceUUID, null, + followLink('eperson'), followLink('group')).pipe( + filter(() => this.isActive), + getSucceededRemoteData(), + take(1) + ).subscribe((result) => { + this.resourcePolicies$.next(result); + }); } @@ -86,7 +108,13 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * Redirect to resource policy creation page */ createResourcePolicy(): void { - this.router.navigate([`../${this.resourceUUID}/create`], { relativeTo: this.route }) + this.router.navigate([`../create`], { + relativeTo: this.route, + queryParams: { + policyTargetId: this.resourceUUID, + targetType: this.resourceType + } + }) } /** @@ -95,7 +123,12 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * @param policy The resource policy */ editResourcePolicy(policy: ResourcePolicy): void { - this.router.navigate([`../${this.resourceUUID}/${policy.id}/edit`], { relativeTo: this.route }) + this.router.navigate([`../edit`], { + relativeTo: this.route, + queryParams: { + policyId: policy.id + } + }) } /** @@ -104,9 +137,11 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * @param policy The resource policy */ getEPersonName(policy: ResourcePolicy): Observable { - return this.ePersonService.findByHref(policy._links.eperson.href).pipe( - getFirstSucceededRemoteDataPayload(), - map((eperson: EPerson) => this.dsoNameService.getName(eperson)) + return policy.eperson.pipe( + filter(() => this.isActive), + getFirstSucceededRemoteDataWithNotEmptyPayload(), + map((eperson: EPerson) => this.dsoNameService.getName(eperson)), + startWith('') ) } @@ -116,9 +151,11 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * @param policy The resource policy */ getGroupName(policy: ResourcePolicy): Observable { - return this.groupService.findByHref(policy._links.group.href).pipe( - getFirstSucceededRemoteDataPayload(), - map((group: Group) => this.dsoNameService.getName(group)) + return policy.group.pipe( + filter(() => this.isActive), + getFirstSucceededRemoteDataWithNotEmptyPayload(), + map((group: Group) => this.dsoNameService.getName(group)), + startWith('') ) } @@ -128,7 +165,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * @return an observable that emits all resource's policies */ getResourcePolicies(): Observable>> { - return this.resourcePolicies$; + return this.resourcePolicies$.asObservable(); } /** @@ -138,7 +175,8 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * @return an observable that emits true when the policy is linked to a ePerson, false otherwise */ hasEPerson(policy): Observable { - return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + return policy.eperson.pipe( + filter(() => this.isActive), getFirstSucceededRemoteDataPayload(), map((eperson: EPerson) => isNotEmpty(eperson)) ) @@ -151,7 +189,8 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * @return an observable that emits true when the policy is linked to a group, false otherwise */ hasGroup(policy): Observable { - return this.groupService.findByHref(policy._links.group.href).pipe( + return policy.group.pipe( + filter(() => this.isActive), getFirstSucceededRemoteDataPayload(), map((group: Group) => isNotEmpty(group)) ) @@ -164,7 +203,8 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ redirectToGroupEditPage(policy: ResourcePolicy): void { this.subs.push( - this.groupService.findByHref(policy._links.group.href).pipe( + policy.group.pipe( + filter(() => this.isActive), getFirstSucceededRemoteDataPayload(), map((group: Group) => group.id) ).subscribe((groupUUID) => this.router.navigate(['groups', groupUUID, 'edit'])) @@ -175,6 +215,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * Unsubscribe from all subscriptions */ ngOnDestroy(): void { + this.isActive = false; this.subs .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()) diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 35eb5de6b5..b7b3a9bbff 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -189,9 +189,9 @@ import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-ve import { ResourcePoliciesComponent } from './resource-policies/resource-policies.component'; import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive'; import { ResourcePolicyFormComponent } from './resource-policies/form/resource-policy-form'; -import { ResourcePolicyCreateComponent } from './resource-policies/create/resource-policy-create.component'; -import { ResourcePolicyEditComponent } from './resource-policies/edit/resource-policy-edit.component'; import { EpersonGroupListComponent } from './resource-policies/form/eperson-group-list/eperson-group-list.component'; +import { ResourcePolicyTargetResolver } from './resource-policies/resolvers/resource-policy-target.resolver'; +import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -366,8 +366,6 @@ const COMPONENTS = [ ItemVersionsNoticeComponent, ResourcePoliciesComponent, ResourcePolicyFormComponent, - ResourcePolicyCreateComponent, - ResourcePolicyEditComponent, EpersonGroupListComponent ]; @@ -435,9 +433,7 @@ const ENTRY_COMPONENTS = [ LogInPasswordComponent, LogInShibbolethComponent, ItemVersionsComponent, - ItemVersionsNoticeComponent, - ResourcePolicyCreateComponent, - ResourcePolicyEditComponent + ItemVersionsNoticeComponent ]; const SHARED_ITEM_PAGE_COMPONENTS = [ @@ -452,7 +448,9 @@ const PROVIDERS = [ { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn - } + }, + ResourcePolicyResolver, + ResourcePolicyTargetResolver ]; const DIRECTIVES = [ From 5cb0570bc0d429330dc9d08d2f4fcb8b2807c989 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 6 Apr 2020 16:27:58 +0200 Subject: [PATCH 023/103] Added new date utils --- src/app/shared/date.util.ts | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/app/shared/date.util.ts b/src/app/shared/date.util.ts index 5e91871967..1c28500d48 100644 --- a/src/app/shared/date.util.ts +++ b/src/app/shared/date.util.ts @@ -3,6 +3,8 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; import { isObject } from 'lodash'; import * as moment from 'moment'; +import { isEmpty } from './empty.util'; + /** * Returns true if the passed value is a NgbDateStruct. * @@ -13,7 +15,7 @@ import * as moment from 'moment'; */ export function isNgbDateStruct(value: object): boolean { return isObject(value) && value.hasOwnProperty('day') - && value.hasOwnProperty('month') && value.hasOwnProperty('year'); + && value.hasOwnProperty('month') && value.hasOwnProperty('year'); } /** @@ -56,3 +58,35 @@ export function dateToISOFormat(date: Date | NgbDateStruct): string { export function ngbDateStructToDate(date: NgbDateStruct): Date { return new Date(date.year, (date.month - 1), date.day); } + +/** + * Returns a NgbDateStruct object started from a string representing a date + * + * @param date + * The Date to convert + * @return NgbDateStruct + * the NgbDateStruct object + */ +export function stringToNgbDateStruct(date: string): NgbDateStruct { + return dateToNgbDateStruct(new Date(date)); +} + +/** + * Returns a NgbDateStruct object started from a Date object + * + * @param date + * The Date to convert + * @return NgbDateStruct + * the NgbDateStruct object + */ +export function dateToNgbDateStruct(date?: Date): NgbDateStruct { + if (isEmpty(date)) { + date = new Date() + } + + return { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate() + }; +} From d003400c16496e29a25b4cf6c9df58fda402da4c Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 6 Apr 2020 17:45:27 +0200 Subject: [PATCH 024/103] started fixing tests --- ...arch-result-grid-element.component.spec.ts | 65 +++-------- ...in-workflow-grid-element.component.spec.ts | 80 ++----------- ...arch-result-list-element.component.spec.ts | 70 +++--------- ...in-workflow-list-element.component.spec.ts | 94 +++++----------- ...m-admin-workflow-actions.component.spec.ts | 106 +++--------------- .../workflow-item-send-back.component.spec.ts | 12 +- 6 files changed, 89 insertions(+), 338 deletions(-) diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts index 136c6df2af..a2d88465d5 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts @@ -14,16 +14,20 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { TaskAdminWorkflowSearchResultGridElementComponent } from './task-admin-workflow-search-result-grid-element.component'; +import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; +import { SearchResult } from '../../../../../shared/search/search-result.model'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; -describe('ItemAdminSearchResultGridElementComponent', () => { +describe('TaskAdminWorkflowSearchResultGridElementComponent', () => { let component: TaskAdminWorkflowSearchResultGridElementComponent; let fixture: ComponentFixture; let id; let searchResult; + let linkService; const mockBitstreamDataService = { getThumbnailFor(item: Item): Observable> { @@ -33,9 +37,11 @@ describe('ItemAdminSearchResultGridElementComponent', () => { function init() { id = '780b2588-bda5-4112-a1cd-0b15000a5339'; - searchResult = new ItemSearchResult(); - searchResult.indexableObject = new Item(); + searchResult = new SearchResult(); + searchResult.indexableObject = new TaskObject(); + searchResult.indexableObject.workflowitem = createSuccessfulRemoteDataObject$(new WorkflowItem()); searchResult.indexableObject.uuid = id; + linkService = getMockLinkService(); } beforeEach(async(() => { @@ -52,6 +58,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { providers: [ { provide: TruncatableService, useValue: mockTruncatableService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + { provide: LinkService, useValue: linkService }, ], schemas: [NO_ERRORS_SCHEMA] }) @@ -59,6 +66,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { })); beforeEach(() => { + linkService.resolveLink.and.callFake((a) => a); fixture = TestBed.createComponent(TaskAdminWorkflowSearchResultGridElementComponent); component = fixture.componentInstance; component.object = searchResult; @@ -71,51 +79,4 @@ describe('ItemAdminSearchResultGridElementComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - describe('when the item is not withdrawn', () => { - beforeEach(() => { - component.dso.isWithdrawn = false; - fixture.detectChanges(); - }); - - it('should not show the withdrawn badge', () => { - const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); - expect(badge).toBeNull(); - }); - }); - - describe('when the item is withdrawn', () => { - beforeEach(() => { - component.dso.isWithdrawn = true; - fixture.detectChanges(); - }); - - it('should show the withdrawn badge', () => { - const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); - expect(badge).not.toBeNull(); - }); - }); - - describe('when the item is not private', () => { - beforeEach(() => { - component.dso.isDiscoverable = true; - fixture.detectChanges(); - }); - it('should not show the private badge', () => { - const badge = fixture.debugElement.query(By.css('div.private-badge')); - expect(badge).toBeNull(); - }); - }); - - describe('when the item is private', () => { - beforeEach(() => { - component.dso.isDiscoverable = false; - fixture.detectChanges(); - }); - - it('should show the private badge', () => { - const badge = fixture.debugElement.query(By.css('div.private-badge')); - expect(badge).not.toBeNull(); - }); - }) }); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts index 61bcdde6cf..917d3770bb 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts @@ -3,39 +3,28 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; -import { Observable } from 'rxjs/internal/Observable'; -import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { Bitstream } from '../../../../../core/shared/bitstream.model'; -import { Item } from '../../../../../core/shared/item.model'; import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; import { SharedModule } from '../../../../../shared/shared.module'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { WorkflowItemAdminWorkflowGridElementComponent } from './workflow-item-admin-workflow-grid-element.component'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; -describe('ItemAdminSearchResultGridElementComponent', () => { +describe('WorkflowItemAdminWorkflowGridElementComponent', () => { let component: WorkflowItemAdminWorkflowGridElementComponent; let fixture: ComponentFixture; let id; - let searchResult; - - const mockBitstreamDataService = { - getThumbnailFor(item: Item): Observable> { - return createSuccessfulRemoteDataObject$(new Bitstream()); - } - }; + let wfi; + let linkService; function init() { id = '780b2588-bda5-4112-a1cd-0b15000a5339'; - searchResult = new ItemSearchResult(); - searchResult.indexableObject = new Item(); - searchResult.indexableObject.uuid = id; + wfi = new WorkflowItem(); + linkService = getMockLinkService(); } beforeEach(async(() => { @@ -50,8 +39,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { SharedModule ], providers: [ - { provide: TruncatableService, useValue: mockTruncatableService }, - { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + { provide: LinkService, useValue: linkService }, ], schemas: [NO_ERRORS_SCHEMA] }) @@ -59,9 +47,10 @@ describe('ItemAdminSearchResultGridElementComponent', () => { })); beforeEach(() => { + linkService.resolveLink.and.callFake((a) => a); fixture = TestBed.createComponent(WorkflowItemAdminWorkflowGridElementComponent); component = fixture.componentInstance; - component.object = searchResult; + component.object = wfi; component.linkTypes = CollectionElementLinkType; component.index = 0; component.viewModes = ViewMode; @@ -71,51 +60,4 @@ describe('ItemAdminSearchResultGridElementComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - describe('when the item is not withdrawn', () => { - beforeEach(() => { - component.dso.isWithdrawn = false; - fixture.detectChanges(); - }); - - it('should not show the withdrawn badge', () => { - const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); - expect(badge).toBeNull(); - }); - }); - - describe('when the item is withdrawn', () => { - beforeEach(() => { - component.dso.isWithdrawn = true; - fixture.detectChanges(); - }); - - it('should show the withdrawn badge', () => { - const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); - expect(badge).not.toBeNull(); - }); - }); - - describe('when the item is not private', () => { - beforeEach(() => { - component.dso.isDiscoverable = true; - fixture.detectChanges(); - }); - it('should not show the private badge', () => { - const badge = fixture.debugElement.query(By.css('div.private-badge')); - expect(badge).toBeNull(); - }); - }); - - describe('when the item is private', () => { - beforeEach(() => { - component.dso.isDiscoverable = false; - fixture.detectChanges(); - }); - - it('should show the private badge', () => { - const badge = fixture.debugElement.query(By.css('div.private-badge')); - expect(badge).not.toBeNull(); - }); - }) }); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts index 684dd8f108..68ffe9e421 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts @@ -2,26 +2,32 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { TaskAdminWorkflowSearchResultListElementComponent } from './task-admin-workflow-search-result-list-element.component'; -import { Item } from '../../../../../core/shared/item.model'; +import { SearchResult } from '../../../../../shared/search/search-result.model'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; -describe('ItemAdminSearchResultListElementComponent', () => { +describe('TaskAdminWorkflowSearchResultListElementComponent', () => { let component: TaskAdminWorkflowSearchResultListElementComponent; let fixture: ComponentFixture; let id; let searchResult; + let linkService; function init() { id = '780b2588-bda5-4112-a1cd-0b15000a5339'; - searchResult = new ItemSearchResult(); - searchResult.indexableObject = new Item(); + searchResult = new SearchResult(); + searchResult.indexableObject = new TaskObject(); + searchResult.indexableObject.workflowitem = createSuccessfulRemoteDataObject$(new WorkflowItem()); searchResult.indexableObject.uuid = id; + linkService = getMockLinkService(); } beforeEach(async(() => { @@ -32,13 +38,16 @@ describe('ItemAdminSearchResultListElementComponent', () => { RouterTestingModule.withRoutes([]) ], declarations: [TaskAdminWorkflowSearchResultListElementComponent], - providers: [{ provide: TruncatableService, useValue: {} }], + providers: [{ provide: TruncatableService, useValue: {} }, + { provide: LinkService, useValue: linkService } + ], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); })); beforeEach(() => { + linkService.resolveLink.and.callFake((a) => a); fixture = TestBed.createComponent(TaskAdminWorkflowSearchResultListElementComponent); component = fixture.componentInstance; component.object = searchResult; @@ -51,51 +60,4 @@ describe('ItemAdminSearchResultListElementComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - describe('when the item is not withdrawn', () => { - beforeEach(() => { - component.dso.isWithdrawn = false; - fixture.detectChanges(); - }); - - it('should not show the withdrawn badge', () => { - const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); - expect(badge).toBeNull(); - }); - }); - - describe('when the item is withdrawn', () => { - beforeEach(() => { - component.dso.isWithdrawn = true; - fixture.detectChanges(); - }); - - it('should show the withdrawn badge', () => { - const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); - expect(badge).not.toBeNull(); - }); - }); - - describe('when the item is not private', () => { - beforeEach(() => { - component.dso.isDiscoverable = true; - fixture.detectChanges(); - }); - it('should not show the private badge', () => { - const badge = fixture.debugElement.query(By.css('div.private-badge')); - expect(badge).toBeNull(); - }); - }); - - describe('when the item is private', () => { - beforeEach(() => { - component.dso.isDiscoverable = false; - fixture.detectChanges(); - }); - - it('should show the private badge', () => { - const badge = fixture.debugElement.query(By.css('div.private-badge')); - expect(badge).not.toBeNull(); - }); - }) }); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts index e406e4c28c..af9bbbc70f 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts @@ -1,47 +1,56 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; +import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; +import { SharedModule } from '../../../../../shared/shared.module'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { WorkflowItemAdminWorkflowListElementComponent } from './workflow-item-admin-workflow-list-element.component'; -import { Item } from '../../../../../core/shared/item.model'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; -describe('ItemAdminSearchResultListElementComponent', () => { +describe('WorkflowItemAdminWorkflowListElementComponent', () => { let component: WorkflowItemAdminWorkflowListElementComponent; let fixture: ComponentFixture; let id; - let searchResult; - + let wfi; + let linkService; function init() { id = '780b2588-bda5-4112-a1cd-0b15000a5339'; - searchResult = new ItemSearchResult(); - searchResult.indexableObject = new Item(); - searchResult.indexableObject.uuid = id; + wfi = new WorkflowItem(); + linkService = getMockLinkService(); } beforeEach(async(() => { init(); - TestBed.configureTestingModule({ - imports: [ - TranslateModule.forRoot(), - RouterTestingModule.withRoutes([]) - ], - declarations: [WorkflowItemAdminWorkflowListElementComponent], - providers: [{ provide: TruncatableService, useValue: {} }], - schemas: [NO_ERRORS_SCHEMA] - }) + TestBed.configureTestingModule( + { + declarations: [WorkflowItemAdminWorkflowListElementComponent], + imports: [ + NoopAnimationsModule, + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]), + SharedModule + ], + providers: [ + { provide: TruncatableService, useValue: mockTruncatableService }, + { provide: LinkService, useValue: linkService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) .compileComponents(); })); beforeEach(() => { + linkService.resolveLink.and.callFake((a) => a); fixture = TestBed.createComponent(WorkflowItemAdminWorkflowListElementComponent); component = fixture.componentInstance; - component.object = searchResult; + component.object = wfi; component.linkTypes = CollectionElementLinkType; component.index = 0; component.viewModes = ViewMode; @@ -51,51 +60,4 @@ describe('ItemAdminSearchResultListElementComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - describe('when the item is not withdrawn', () => { - beforeEach(() => { - component.dso.isWithdrawn = false; - fixture.detectChanges(); - }); - - it('should not show the withdrawn badge', () => { - const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); - expect(badge).toBeNull(); - }); - }); - - describe('when the item is withdrawn', () => { - beforeEach(() => { - component.dso.isWithdrawn = true; - fixture.detectChanges(); - }); - - it('should show the withdrawn badge', () => { - const badge = fixture.debugElement.query(By.css('div.withdrawn-badge')); - expect(badge).not.toBeNull(); - }); - }); - - describe('when the item is not private', () => { - beforeEach(() => { - component.dso.isDiscoverable = true; - fixture.detectChanges(); - }); - it('should not show the private badge', () => { - const badge = fixture.debugElement.query(By.css('div.private-badge')); - expect(badge).toBeNull(); - }); - }); - - describe('when the item is private', () => { - beforeEach(() => { - component.dso.isDiscoverable = false; - fixture.detectChanges(); - }); - - it('should show the private badge', () => { - const badge = fixture.debugElement.query(By.css('div.private-badge')); - expect(badge).not.toBeNull(); - }); - }) }); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts index 2ee8ae2e9d..bca2684364 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts @@ -4,7 +4,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { ItemAdminWorkflowSearchResultActionsComponent } from './workflow-item-admin-workflow-actions.component'; import { Item } from '../../../core/shared/item.model'; import { ITEM_EDIT_DELETE_PATH, @@ -16,17 +15,20 @@ import { } from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; import { getItemEditPath } from '../../../+item-page/item-page-routing.module'; import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { WorkflowItemAdminWorkflowActionsComponent } from './workflow-item-admin-workflow-actions.component'; +import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; +import { getWorkflowItemDeletePath, getWorkflowItemSendBackPath } from '../../../+workflowitems-edit-page/workflowitems-edit-page-routing.module'; -describe('ItemAdminSearchResultActionsComponent', () => { - let component: ItemAdminWorkflowSearchResultActionsComponent; - let fixture: ComponentFixture; +describe('WorkflowItemAdminWorkflowActionsComponent', () => { + let component: WorkflowItemAdminWorkflowActionsComponent; + let fixture: ComponentFixture; let id; - let item; + let wfi; function init() { id = '780b2588-bda5-4112-a1cd-0b15000a5339'; - item = new Item(); - item.uuid = id; + wfi = new WorkflowItem(); + wfi.id = id; } beforeEach(async(() => { init(); @@ -35,16 +37,16 @@ describe('ItemAdminSearchResultActionsComponent', () => { TranslateModule.forRoot(), RouterTestingModule.withRoutes([]) ], - declarations: [ItemAdminWorkflowSearchResultActionsComponent], + declarations: [WorkflowItemAdminWorkflowActionsComponent], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ItemAdminWorkflowSearchResultActionsComponent); + fixture = TestBed.createComponent(WorkflowItemAdminWorkflowActionsComponent); component = fixture.componentInstance; - component.item = item; + component.wfi = wfi; fixture.detectChanges(); }); @@ -52,93 +54,15 @@ describe('ItemAdminSearchResultActionsComponent', () => { expect(component).toBeTruthy(); }); - it('should render an edit button with the correct link', () => { - const button = fixture.debugElement.query(By.css('a.edit-link')); - const link = button.nativeElement.href; - expect(link).toContain(getItemEditPath(id)); - }); - it('should render a delete button with the correct link', () => { const button = fixture.debugElement.query(By.css('a.delete-link')); const link = button.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_DELETE_PATH).toString()); + expect(link).toContain(new URLCombiner(getWorkflowItemDeletePath(wfi.id)).toString()); }); it('should render a move button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a.move-link')); + const a = fixture.debugElement.query(By.css('a.send-back-link')); const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_MOVE_PATH).toString()); + expect(link).toContain(new URLCombiner(getWorkflowItemSendBackPath(wfi.id)).toString()); }); - - describe('when the item is not withdrawn', () => { - beforeEach(() => { - component.item.isWithdrawn = false; - fixture.detectChanges(); - }); - - it('should render a withdraw button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a.withdraw-link')); - const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_WITHDRAW_PATH).toString()); - }); - - it('should not render a reinstate button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a.reinstate-link')); - expect(a).toBeNull(); - }); - }); - - describe('when the item is withdrawn', () => { - beforeEach(() => { - component.item.isWithdrawn = true; - fixture.detectChanges(); - }); - - it('should not render a withdraw button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a.withdraw-link')); - expect(a).toBeNull(); - }); - - it('should render a reinstate button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a.reinstate-link')); - const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_REINSTATE_PATH).toString()); - }); - }); - - describe('when the item is not private', () => { - beforeEach(() => { - component.item.isDiscoverable = true; - fixture.detectChanges(); - }); - - it('should render a make private button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a.private-link')); - const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PRIVATE_PATH).toString()); - }); - - it('should not render a make public button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a.public-link')); - expect(a).toBeNull(); - }); - }); - - describe('when the item is private', () => { - beforeEach(() => { - component.item.isDiscoverable = false; - fixture.detectChanges(); - }); - - it('should not render a make private button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a.private-link')); - expect(a).toBeNull(); - }); - - it('should render a make private button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a.public-link')); - const link = a.nativeElement.href; - expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PUBLIC_PATH).toString()); - }); - }) }); diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts index ba293845d9..e76e01b7f3 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts @@ -1,20 +1,20 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; -import { WorkflowItemDeleteComponent } from './workflow-item-send-back.component'; -describe('WorkflowItemDeleteComponent', () => { - let component: WorkflowItemDeleteComponent; - let fixture: ComponentFixture; +describe('WorkflowItemSendBackComponent', () => { + let component: WorkflowItemSendBackComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ WorkflowItemDeleteComponent ] + declarations: [ WorkflowItemSendBackComponent ] }) .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(WorkflowItemDeleteComponent); + fixture = TestBed.createComponent(WorkflowItemSendBackComponent); component = fixture.componentInstance; fixture.detectChanges(); }); From 4c391bbae4e3c8e6970c60566f6ea9b08e0f559e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 6 Apr 2020 20:11:20 +0200 Subject: [PATCH 025/103] Fixed issue with delete operation --- src/app/core/data/data.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 26af540193..ca59daa5af 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -506,7 +506,7 @@ export abstract class DataService { const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata); return this.requestService.getByUUID(requestId).pipe( - find((request: RequestEntry) => request.completed), + find((request: RequestEntry) => isNotEmpty(request) && request.completed), map((request: RequestEntry) => request.response.isSuccessful) ); } From b210a36e53a4bab11c7d8505435f493da347f22e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 6 Apr 2020 20:11:37 +0200 Subject: [PATCH 026/103] Added new date utils --- src/app/shared/date.util.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/app/shared/date.util.ts b/src/app/shared/date.util.ts index 1c28500d48..afbdabc856 100644 --- a/src/app/shared/date.util.ts +++ b/src/app/shared/date.util.ts @@ -3,7 +3,7 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; import { isObject } from 'lodash'; import * as moment from 'moment'; -import { isEmpty } from './empty.util'; +import { isNull } from './empty.util'; /** * Returns true if the passed value is a NgbDateStruct. @@ -80,7 +80,7 @@ export function stringToNgbDateStruct(date: string): NgbDateStruct { * the NgbDateStruct object */ export function dateToNgbDateStruct(date?: Date): NgbDateStruct { - if (isEmpty(date)) { + if (isNull(date)) { date = new Date() } @@ -90,3 +90,25 @@ export function dateToNgbDateStruct(date?: Date): NgbDateStruct { day: date.getDate() }; } + +/** + * Returns a date in simplified format (YYYY-MM-DD). + * + * @param date + * The date to format + * @return string + * the formatted date + */ +export function dateToString(date: Date | NgbDateStruct): string { + const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date); + + let year = dateObj.getFullYear().toString(); + let month = (dateObj.getMonth() + 1).toString(); + let day = dateObj.getDate().toString(); + + year = (year.length === 1) ? '0' + year : year; + month = (month.length === 1) ? '0' + month : month; + day = (day.length === 1) ? '0' + day : day; + const dateStr = `${year}-${month}-${day}`; + return moment.utc(dateStr, 'YYYYMMDD').format('YYYY-MM-DD'); +} From e078071dfbe28a26445a4934a65105c208cbb32b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 6 Apr 2020 20:36:56 +0200 Subject: [PATCH 027/103] Intermediate commit --- resources/i18n/en.json5 | 6 + .../item-authorizations.component.html | 2 +- .../item-authorizations.component.ts | 30 ++- src/app/+item-page/item-page.resolver.ts | 3 +- .../resource-policy-create.component.html | 5 +- .../resource-policy-create.component.ts | 32 ++- .../edit/resource-policy-edit.component.html | 1 + .../edit/resource-policy-edit.component.ts | 25 +- .../eperson-group-list.component.ts | 1 + .../form/resource-policy-form.html | 12 +- .../form/resource-policy-form.model.ts | 14 +- .../form/resource-policy-form.ts | 72 +++-- .../resource-policies.component.html | 77 ++++-- .../resource-policies.component.ts | 254 ++++++++++++------ 14 files changed, 383 insertions(+), 151 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index c55865f3b8..1077ff9e15 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1812,6 +1812,12 @@ "resource-policies.create.page.title": "Create new resource policy", + "resource-policies.delete.btn": "Delete selected resource policies", + + "resource-policies.delete.failure.content": "An error occurred while deleting selected resource policies.", + + "resource-policies.delete.success.content": "Operation successful", + "resource-policies.edit.page.heading": "Edit resource policy ", "resource-policies.edit.page.failure.content": "An error occurred while editing the resource policy.", diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html index 0cf61579f1..71aa7b44de 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html @@ -1,7 +1,7 @@
- + diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index e241166a47..3322d4cc36 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -1,12 +1,15 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; import { catchError, filter, first, flatMap, map, take } from 'rxjs/operators'; import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; import { PaginatedList } from '../../../core/data/paginated-list'; -import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteDataWithNotEmptyPayload +} from '../../../core/shared/operators'; import { Item } from '../../../core/shared/item.model'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { LinkService } from '../../../core/cache/builders/link.service'; @@ -15,6 +18,9 @@ import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { Bitstream } from '../../../core/shared/bitstream.model'; import { FindListOptions } from '../../../core/data/request.models'; +/** + * Interface for a bundle's bitstream map entry + */ interface BundleBitstreamsMapEntry { id: string; bitstreams: Observable> @@ -35,7 +41,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * The list of bundle for the item * @type {Observable>} */ - private bundles$: Observable>; + private bundles$: BehaviorSubject = new BehaviorSubject([]); /** * The target editing item @@ -69,22 +75,28 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { ngOnInit(): void { this.item$ = this.route.data.pipe( map((data) => data.item), - getFirstSucceededRemoteDataPayload(), + getFirstSucceededRemoteDataWithNotEmptyPayload(), map((item: Item) => this.linkService.resolveLink( item, followLink('bundles', new FindListOptions(), true, followLink('bitstreams')) )) ) as Observable; - this.bundles$ = this.item$.pipe( + const bundles$: Observable> = this.item$.pipe( filter((item: Item) => isNotEmpty(item.bundles)), flatMap((item: Item) => item.bundles), - getFirstSucceededRemoteDataPayload(), + getFirstSucceededRemoteDataWithNotEmptyPayload(), catchError(() => observableOf(new PaginatedList(null, []))) ); this.subs.push( - this.bundles$.pipe( + bundles$.pipe( + take(1), + map((list: PaginatedList) => list.page) + ).subscribe((bundles: Bundle[]) => { + this.bundles$.next(bundles); + }), + bundles$.pipe( take(1), flatMap((list: PaginatedList) => list.page), map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) })) @@ -109,8 +121,8 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * * @return an observable that emits all item's bundles */ - getItemBundles(): Observable> { - return this.bundles$ + getItemBundles(): Observable { + return this.bundles$.asObservable(); } /** diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index 501bb34d2c..63a560778b 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -7,6 +7,7 @@ import { Item } from '../core/shared/item.model'; import { hasValue } from '../shared/empty.util'; import { find } from 'rxjs/operators'; import { followLink } from '../shared/utils/follow-link-config.model'; +import { FindListOptions } from '../core/data/request.models'; /** * This class represents a resolver that requests a specific item before the route is activated @@ -26,7 +27,7 @@ export class ItemPageResolver implements Resolve> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { return this.itemService.findById(route.params.id, followLink('owningCollection'), - followLink('bundles'), + followLink('bundles', new FindListOptions(), true, followLink('bitstreams')), followLink('relationships'), followLink('version', undefined, true, followLink('versionhistory')), ).pipe( diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.html b/src/app/shared/resource-policies/create/resource-policy-create.component.html index c4eb42bb18..85d0d13e96 100644 --- a/src/app/shared/resource-policies/create/resource-policy-create.component.html +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.html @@ -1,6 +1,7 @@
-

{{'resource-policies.edit.page.heading' | translate}} {{targetResourceName}}

+

{{'resource-policies.create.page.heading' | translate}} {{targetResourceName}}

-
diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.ts b/src/app/shared/resource-policies/create/resource-policy-create.component.ts index 375966509b..4785e39222 100644 --- a/src/app/shared/resource-policies/create/resource-policy-create.component.ts +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.ts @@ -1,7 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, Observable } from 'rxjs'; import { first, map, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; @@ -18,19 +20,29 @@ import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; }) export class ResourcePolicyCreateComponent implements OnInit { + /** + * The name of the resource target of the policy + */ + public targetResourceName: string; + + /** + * A boolean representing if a submission creation operation is pending + * @type {BehaviorSubject} + */ + private processing$ = new BehaviorSubject(false); + /** * The uuid of the resource target of the policy */ private targetResourceUUID: string; - public targetResourceName: string; - constructor( private dsoNameService: DSONameService, private notificationsService: NotificationsService, private resourcePolicyService: ResourcePolicyService, private route: ActivatedRoute, - private router: Router) { + private router: Router, + private translate: TranslateService) { } ngOnInit(): void { @@ -43,11 +55,16 @@ export class ResourcePolicyCreateComponent implements OnInit { }); } - redirectToAuthorizationsPage() { + isProcessing(): Observable { + return this.processing$.asObservable(); + } + + redirectToAuthorizationsPage(): void { this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route }); } - createResourcePolicy(event: ResourcePolicyEvent) { + createResourcePolicy(event: ResourcePolicyEvent): void { + this.processing$.next(true); let response$; if (event.target.type === 'eperson') { response$ = this.resourcePolicyService.create(event.object, this.targetResourceUUID, event.target.uuid); @@ -57,11 +74,12 @@ export class ResourcePolicyCreateComponent implements OnInit { response$.pipe( first((response: RemoteData) => !response.isResponsePending) ).subscribe((responseRD: RemoteData) => { + this.processing$.next(false); if (responseRD.hasSucceeded) { - this.notificationsService.success(null, 'resource-policies.create.page.success.content'); + this.notificationsService.success(null, this.translate.get('resource-policies.create.page.success.content')); this.redirectToAuthorizationsPage(); } else { - this.notificationsService.error(null, 'resource-policies.create.page.failure.content'); + this.notificationsService.success(null, this.translate.get('resource-policies.create.page.failure.content')); } }) } diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.html b/src/app/shared/resource-policies/edit/resource-policy-edit.component.html index ede5519c74..0f285c4948 100644 --- a/src/app/shared/resource-policies/edit/resource-policy-edit.component.html +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.html @@ -2,6 +2,7 @@

{{'resource-policies.edit.page.heading' | translate}} {{resourcePolicy.id}}

diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts index 844ac5b4e4..20f2a5a34e 100644 --- a/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts @@ -1,7 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, Observable } from 'rxjs'; import { first, map, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; import { NotificationsService } from '../../notifications/notifications.service'; @@ -9,6 +11,7 @@ import { RemoteData } from '../../../core/data/remote-data'; import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; import { ResourcePolicyEvent } from '../form/resource-policy-form'; import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; +import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type'; @Component({ selector: 'ds-resource-policy-edit', @@ -21,11 +24,18 @@ export class ResourcePolicyEditComponent implements OnInit { */ public resourcePolicy: ResourcePolicy; + /** + * A boolean representing if a submission editing operation is pending + * @type {BehaviorSubject} + */ + private processing$ = new BehaviorSubject(false); + constructor( private notificationsService: NotificationsService, private resourcePolicyService: ResourcePolicyService, private route: ActivatedRoute, - private router: Router) { + private router: Router, + private translate: TranslateService) { } ngOnInit(): void { @@ -34,26 +44,33 @@ export class ResourcePolicyEditComponent implements OnInit { take(1) ).subscribe((data: any) => { this.resourcePolicy = (data.resourcePolicy as RemoteData).payload; - console.log(data) }); } + isProcessing(): Observable { + return this.processing$.asObservable(); + } + redirectToAuthorizationsPage() { this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route }); } updateResourcePolicy(event: ResourcePolicyEvent) { + this.processing$.next(true); const updatedObject = Object.assign({}, event.object, { + id: this.resourcePolicy.id, + type: RESOURCE_POLICY.value, _links: this.resourcePolicy._links }); this.resourcePolicyService.update(updatedObject).pipe( first((response: RemoteData) => !response.isResponsePending) ).subscribe((responseRD: RemoteData) => { + this.processing$.next(false); if (responseRD.hasSucceeded) { - this.notificationsService.success(null, 'resource-policies.edit.page.success.content'); + this.notificationsService.success(null, this.translate.get('resource-policies.edit.page.success.content')); this.redirectToAuthorizationsPage(); } else { - this.notificationsService.error(null, 'resource-policies.edit.page.failure.content'); + this.notificationsService.error(null, this.translate.get('resource-policies.edit.page.failure.content')); } }) } diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts index 2b4572cba5..b9e1259501 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts @@ -151,6 +151,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { * Unsubscribe from all subscriptions */ ngOnDestroy(): void { + this.list$ = null; this.subs .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()) diff --git a/src/app/shared/resource-policies/form/resource-policy-form.html b/src/app/shared/resource-policies/form/resource-policy-form.html index 999e7cf66c..6585755145 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.html +++ b/src/app/shared/resource-policies/form/resource-policy-form.html @@ -27,11 +27,19 @@
+ [disabled]="!(isFormValid() | async) || (isProcessing | async)" + (click)="onSubmit()"> + + {{'submission.workflow.tasks.generic.processing' | translate}} + + + {{'form.submit' | translate}} + +
diff --git a/src/app/shared/resource-policies/form/resource-policy-form.model.ts b/src/app/shared/resource-policies/form/resource-policy-form.model.ts index 3192946c9b..37f9b866a9 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.model.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.model.ts @@ -1,5 +1,5 @@ import { - DynamicDateControlModelConfig, + DynamicDatePickerModelConfig, DynamicFormControlLayout, DynamicFormGroupModelConfig, DynamicFormOptionConfig, @@ -118,9 +118,12 @@ export const RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT: DynamicFormControlLayout = } }; -export const RESOURCE_POLICY_FORM_START_DATE_CONFIG: DynamicDateControlModelConfig = { +export const RESOURCE_POLICY_FORM_START_DATE_CONFIG: DynamicDatePickerModelConfig = { id: 'start', label: 'resource-policies.form.date.start.label', + placeholder: 'resource-policies.form.date.start.label', + inline: false, + toggleIcon: 'far fa-calendar-alt' }; export const RESOURCE_POLICY_FORM_START_DATE_LAYOUT: DynamicFormControlLayout = { @@ -133,9 +136,12 @@ export const RESOURCE_POLICY_FORM_START_DATE_LAYOUT: DynamicFormControlLayout = } }; -export const RESOURCE_POLICY_FORM_END_DATE_CONFIG: DynamicDateControlModelConfig = { +export const RESOURCE_POLICY_FORM_END_DATE_CONFIG: DynamicDatePickerModelConfig = { id: 'end', - label: 'resource-policies.form.date.end.label' + label: 'resource-policies.form.date.end.label', + placeholder: 'resource-policies.form.date.end.label', + inline: false, + toggleIcon: 'far fa-calendar-alt' }; export const RESOURCE_POLICY_FORM_END_DATE_LAYOUT: DynamicFormControlLayout = { element: { diff --git a/src/app/shared/resource-policies/form/resource-policy-form.ts b/src/app/shared/resource-policies/form/resource-policy-form.ts index e5b29cdaf1..b8316112e9 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.ts @@ -1,8 +1,13 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { DynamicFormControlModel, DynamicFormGroupModel, DynamicSelectModel } from '@ng-dynamic-forms/core'; +import { Observable, of as observableOf, race as observableRace } from 'rxjs'; +import { filter, map, take } from 'rxjs/operators'; +import { + DynamicDatePickerModel, + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicSelectModel +} from '@ng-dynamic-forms/core'; import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; import { DsDynamicInputModel } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; @@ -19,7 +24,6 @@ import { RESOURCE_POLICY_FORM_START_DATE_LAYOUT } from './resource-policy-form.model'; import { DsDynamicTextAreaModel } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; -import { DynamicDsDatePickerModel } from '../../form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { hasValue, isEmpty, isNotEmpty } from '../../empty.util'; @@ -27,6 +31,11 @@ import { FormService } from '../../form/form.service'; import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type'; import { RemoteData } from '../../../core/data/remote-data'; import { Subscription } from 'rxjs/internal/Subscription'; +import { dateToISOFormat, stringToNgbDateStruct } from '../../date.util'; +import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { RequestService } from '../../../core/data/request.service'; export interface ResourcePolicyEvent { object: ResourcePolicy, @@ -51,6 +60,12 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { */ @Input() resourcePolicy: ResourcePolicy; + /** + * A boolean representing if form submit operation is processing + * @type {boolean} + */ + @Input() isProcessing: Observable = observableOf(false); + /** * An event fired when form is canceled. * Event's payload is empty. @@ -87,6 +102,12 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { */ public resourcePolicyGrantType: string; + /** + * A boolean representing if component is active + * @type {boolean} + */ + private isActive: boolean; + /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} @@ -97,11 +118,17 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { * Initialize instance variables * * @param {DSONameService} dsoNameService + * @param {EPersonDataService} ePersonService * @param {FormService} formService + * @param {GroupDataService} groupService + * @param {RequestService} requestService */ constructor( private dsoNameService: DSONameService, + private ePersonService: EPersonDataService, private formService: FormService, + private groupService: GroupDataService, + private requestService: RequestService, ) { } @@ -109,13 +136,24 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { * Initialize the component, setting up the form model */ ngOnInit(): void { + this.isActive = true; this.formId = this.formService.getUniqueId('resource-policy-form'); this.formModel = this.buildResourcePolicyForm(); if (!this.canSetGrant()) { - this.subs.push(observableCombineLatest([this.resourcePolicy.eperson, this.resourcePolicy.group]) - .subscribe(([epersonRD, groupRD]: [RemoteData, RemoteData]) => { - this.resourcePolicyGrant = epersonRD.payload || groupRD.payload; + this.requestService.removeByHrefSubstring(this.resourcePolicy._links.eperson.href); + this.requestService.removeByHrefSubstring(this.resourcePolicy._links.group.href); + const epersonRD$ = this.ePersonService.findByHref(this.resourcePolicy._links.eperson.href).pipe( + getSucceededRemoteData() + ); + const groupRD$ = this.groupService.findByHref(this.resourcePolicy._links.group.href).pipe( + getSucceededRemoteData() + ); + this.subs.push( + observableRace(epersonRD$, groupRD$).pipe( + filter(() => this.isActive), + ).subscribe((dsoRD: RemoteData) => { + this.resourcePolicyGrant = dsoRD.payload; }) ) } @@ -146,15 +184,15 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { new DynamicSelectModel(RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG) ); - const startDateModel = new DynamicDsDatePickerModel( + const startDateModel = new DynamicDatePickerModel( RESOURCE_POLICY_FORM_START_DATE_CONFIG, RESOURCE_POLICY_FORM_START_DATE_LAYOUT ); - const endDateModel = new DynamicDsDatePickerModel( + const endDateModel = new DynamicDatePickerModel( RESOURCE_POLICY_FORM_END_DATE_CONFIG, RESOURCE_POLICY_FORM_END_DATE_LAYOUT ); - const dateGroupConfig = Object.assign({}, RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG); + const dateGroupConfig = Object.assign({}, RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG, { group: [] }); dateGroupConfig.group.push(startDateModel, endDateModel); formModel.push(new DynamicFormGroupModel(dateGroupConfig, RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT)); @@ -172,10 +210,10 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { formModel.forEach((model: any) => { if (model.id === 'date') { if (hasValue(this.resourcePolicy.startDate)) { - model.get(0).valueUpdates.next(this.resourcePolicy.startDate); + model.get(0).valueUpdates.next(stringToNgbDateStruct(this.resourcePolicy.startDate)); } if (hasValue(this.resourcePolicy.endDate)) { - model.get(1).valueUpdates.next(this.resourcePolicy.startDate); + model.get(1).valueUpdates.next(stringToNgbDateStruct(this.resourcePolicy.endDate)); } } else { if (this.resourcePolicy.hasOwnProperty(model.id) && this.resourcePolicy[model.id]) { @@ -203,7 +241,6 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { * @return the object name */ getResourcePolicyTargetName(): string { - console.log(this.resourcePolicy); return isNotEmpty(this.resourcePolicyGrant) ? this.dsoNameService.getName(this.resourcePolicyGrant) : ''; } @@ -228,11 +265,10 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { * Emit a new submit Event whether the form is valid */ onSubmit(): void { - this.formService.getFormData(this.formId) + this.formService.getFormData(this.formId).pipe(take(1)) .subscribe((data) => { const eventPayload: ResourcePolicyEvent = Object.create({}); eventPayload.object = this.createResourcePolicyByFormData(data); - console.log('resourcePolicyTarget', this.resourcePolicyGrant.type.value); eventPayload.target = { type: this.resourcePolicyGrantType, uuid: this.resourcePolicyGrant.id @@ -252,8 +288,8 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { resourcePolicy.description = (data.description) ? data.description[0].value : null; resourcePolicy.policyType = (data.policyType) ? data.policyType[0].value : null; resourcePolicy.action = (data.action) ? data.action[0].value : null; - resourcePolicy.startDate = (data.date && data.date.start) ? data.date.start[0].value : null; - resourcePolicy.endDate = (data.date && data.date.end) ? data.date.end[0].value : null; + resourcePolicy.startDate = (data.date && data.date.start) ? dateToISOFormat(data.date.start[0].value) : null; + resourcePolicy.endDate = (data.date && data.date.end) ? dateToISOFormat(data.date.end[0].value) : null; resourcePolicy.type = RESOURCE_POLICY; return resourcePolicy; @@ -263,6 +299,8 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { * Unsubscribe from all subscriptions */ ngOnDestroy(): void { + this.isActive = false; + this.formModel = null; this.subs .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()) diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index 19303b67ed..8209c836ff 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -1,17 +1,40 @@ -
+
- + @@ -23,29 +46,39 @@ - + + - - - - + + + - - + - - - + + +
-

+

+
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} {{resourceUUID}} - -

+
+ + +
+
+
+ + +
+
{{'resource-policies.table.headers.id' | translate}} {{'resource-policies.table.headers.name' | translate}} {{'resource-policies.table.headers.policyType' | translate}}
+
+ + +
+
- {{policy.id}} - {{policy.name}}{{policy.policyType}}{{policy.action}} - {{getEPersonName(policy) | async}} + {{entry.policy.name}}{{entry.policy.policyType}}{{entry.policy.action}} + {{getEPersonName(entry.policy) | async}} - {{getGroupName(policy) | async}} - + {{getGroupName(entry.policy) | async}} + {{policy.startDate}}{{policy.endDate}}{{formatDate(entry.policy.startDate)}}{{formatDate(entry.policy.endDate)}}
diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts index 257657afdd..8ad11315a4 100644 --- a/src/app/shared/resource-policies/resource-policies.component.ts +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -1,26 +1,32 @@ import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { filter, map, startWith, take } from 'rxjs/operators'; +import { BehaviorSubject, from as observableFrom, Observable, Subscription } from 'rxjs'; +import { concatMap, distinctUntilChanged, filter, map, reduce, scan, startWith, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service'; -import { PaginatedList } from '../../core/data/paginated-list'; import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataWithNotEmptyPayload, getSucceededRemoteData } from '../../core/shared/operators'; -import { RemoteData } from '../../core/data/remote-data'; import { ResourcePolicy } from '../../core/resource-policy/models/resource-policy.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { Group } from '../../core/eperson/models/group.model'; import { GroupDataService } from '../../core/eperson/group-data.service'; -import { hasValue, isNotEmpty } from '../empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; -import { followLink } from '../utils/follow-link-config.model'; import { RequestService } from '../../core/data/request.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { dateToString, stringToNgbDateStruct } from '../date.util'; + +interface ResourcePolicyCheckboxEntry { + id: string; + policy: ResourcePolicy; + checked: boolean +} @Component({ selector: 'ds-resource-policies', @@ -51,11 +57,17 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { private isActive: boolean; /** - * The list of policies for given resource - * @type {Observable>>} + * A boolean representing if a submission delete operation is pending + * @type {BehaviorSubject} */ - private resourcePolicies$: BehaviorSubject>> = - new BehaviorSubject>>({} as any); + private processingDelete$ = new BehaviorSubject(false); + + /** + * The list of policies for given resource + * @type {BehaviorSubject} + */ + private resourcePoliciesEntries$: BehaviorSubject = + new BehaviorSubject([]); /** * Array to track all subscriptions and unsubscribe them onDestroy @@ -70,20 +82,24 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * @param {DSONameService} dsoNameService * @param {EPersonDataService} ePersonService * @param {GroupDataService} groupService + * @param {NotificationsService} notificationsService * @param {RequestService} requestService * @param {ResourcePolicyService} resourcePolicyService * @param {ActivatedRoute} route * @param {Router} router + * @param {TranslateService} translate */ constructor( private cdr: ChangeDetectorRef, private dsoNameService: DSONameService, private ePersonService: EPersonDataService, private groupService: GroupDataService, + private notificationsService: NotificationsService, private requestService: RequestService, private resourcePolicyService: ResourcePolicyService, private route: ActivatedRoute, - private router: Router + private router: Router, + private translate: TranslateService ) { } @@ -92,22 +108,144 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ ngOnInit(): void { this.isActive = true; + this.initResourcePolicyLIst(); + } + + canDelete(): Observable { + return observableFrom(this.resourcePoliciesEntries$.value).pipe( + filter((entry: ResourcePolicyCheckboxEntry) => entry.checked), + reduce((acc: any, value: any) => [...acc, ...value], []), + map((entries: ResourcePolicyCheckboxEntry[]) => isNotEmpty(entries)), + distinctUntilChanged() + ) + } + + deleteSelectedResourcePolicies(): void { + this.processingDelete$.next(true); + const policiesToDelete: ResourcePolicyCheckboxEntry[] = this.resourcePoliciesEntries$.value + .filter((entry: ResourcePolicyCheckboxEntry) => entry.checked); + observableFrom(policiesToDelete).pipe( + concatMap((entry: ResourcePolicyCheckboxEntry) => this.resourcePolicyService.delete(entry.policy.id)), + scan((acc: any, value: any) => [...acc, ...value], []), + filter((results: boolean[]) => results.length === policiesToDelete.length), + take(1), + ).subscribe((results: boolean[]) => { + const failureResults = results.filter((result: boolean) => !result); + if (isEmpty(failureResults)) { + this.notificationsService.success(null, this.translate.get('resource-policies.delete.success.content')); + } else { + this.notificationsService.error(null, this.translate.get('resource-policies.delete.failure.content')); + } + this.initResourcePolicyLIst(); + this.processingDelete$.next(false); + }) + } + + /** + * Returns a date in simplified format (YYYY-MM-DD). + * + * @param date + * @return a string with formatted date + */ + formatDate(date: string): string { + return isNotEmpty(date) ? dateToString(stringToNgbDateStruct(date)) : ''; + } + + /** + * Return the ePerson's name which the given policy is linked to + * + * @param policy The resource policy + */ + getEPersonName(policy: ResourcePolicy): Observable { + // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved + return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + filter(() => this.isActive), + getFirstSucceededRemoteDataWithNotEmptyPayload(), + map((eperson: EPerson) => this.dsoNameService.getName(eperson)), + startWith('') + ) + } + + /** + * Return the group's name which the given policy is linked to + * + * @param policy The resource policy + */ + getGroupName(policy: ResourcePolicy): Observable { + // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved + return this.groupService.findByHref(policy._links.group.href).pipe( + filter(() => this.isActive), + getFirstSucceededRemoteDataWithNotEmptyPayload(), + map((group: Group) => this.dsoNameService.getName(group)), + startWith('') + ) + } + + /** + * Return all resource's policies + * + * @return an observable that emits all resource's policies + */ + getResourcePolicies(): Observable { + return this.resourcePoliciesEntries$.asObservable(); + } + + /** + * Check whether the given policy is linked to a ePerson + * + * @param policy The resource policy + * @return an observable that emits true when the policy is linked to a ePerson, false otherwise + */ + hasEPerson(policy): Observable { + // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved + return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + filter(() => this.isActive), + getFirstSucceededRemoteDataPayload(), + map((eperson: EPerson) => isNotEmpty(eperson)) + ) + } + + /** + * Check whether the given policy is linked to a group + * + * @param policy The resource policy + * @return an observable that emits true when the policy is linked to a group, false otherwise + */ + hasGroup(policy): Observable { + // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved + return this.groupService.findByHref(policy._links.group.href).pipe( + filter(() => this.isActive), + getFirstSucceededRemoteDataPayload(), + map((group: Group) => isNotEmpty(group)) + ) + } + + initResourcePolicyLIst() { + // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved this.requestService.removeByHrefSubstring(this.resourceUUID); - this.resourcePolicyService.searchByResource(this.resourceUUID, null, - followLink('eperson'), followLink('group')).pipe( + this.resourcePolicyService.searchByResource(this.resourceUUID).pipe( filter(() => this.isActive), getSucceededRemoteData(), take(1) ).subscribe((result) => { - this.resourcePolicies$.next(result); + const entries = result.payload.page.map((policy: ResourcePolicy) => ({ + id: policy.id, + policy: policy, + checked: false + })); + this.resourcePoliciesEntries$.next(entries); + this.cdr.detectChanges(); }); + } + isProcessingDelete(): Observable { + return this.processingDelete$.asObservable(); } /** * Redirect to resource policy creation page */ - createResourcePolicy(): void { + redirectToResourcePolicyCreatePage(): void { this.router.navigate([`../create`], { relativeTo: this.route, queryParams: { @@ -122,7 +260,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * * @param policy The resource policy */ - editResourcePolicy(policy: ResourcePolicy): void { + redirectToResourcePolicyEditPage(policy: ResourcePolicy): void { this.router.navigate([`../edit`], { relativeTo: this.route, queryParams: { @@ -131,79 +269,15 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { }) } - /** - * Return the ePerson's name which the given policy is linked to - * - * @param policy The resource policy - */ - getEPersonName(policy: ResourcePolicy): Observable { - return policy.eperson.pipe( - filter(() => this.isActive), - getFirstSucceededRemoteDataWithNotEmptyPayload(), - map((eperson: EPerson) => this.dsoNameService.getName(eperson)), - startWith('') - ) - } - - /** - * Return the group's name which the given policy is linked to - * - * @param policy The resource policy - */ - getGroupName(policy: ResourcePolicy): Observable { - return policy.group.pipe( - filter(() => this.isActive), - getFirstSucceededRemoteDataWithNotEmptyPayload(), - map((group: Group) => this.dsoNameService.getName(group)), - startWith('') - ) - } - - /** - * Return all resource's policies - * - * @return an observable that emits all resource's policies - */ - getResourcePolicies(): Observable>> { - return this.resourcePolicies$.asObservable(); - } - - /** - * Check whether the given policy is linked to a ePerson - * - * @param policy The resource policy - * @return an observable that emits true when the policy is linked to a ePerson, false otherwise - */ - hasEPerson(policy): Observable { - return policy.eperson.pipe( - filter(() => this.isActive), - getFirstSucceededRemoteDataPayload(), - map((eperson: EPerson) => isNotEmpty(eperson)) - ) - } - - /** - * Check whether the given policy is linked to a group - * - * @param policy The resource policy - * @return an observable that emits true when the policy is linked to a group, false otherwise - */ - hasGroup(policy): Observable { - return policy.group.pipe( - filter(() => this.isActive), - getFirstSucceededRemoteDataPayload(), - map((group: Group) => isNotEmpty(group)) - ) - } - /** * Redirect to group edit page * * @param policy The resource policy */ redirectToGroupEditPage(policy: ResourcePolicy): void { + this.requestService.removeByHrefSubstring(policy._links.group.href); this.subs.push( - policy.group.pipe( + this.groupService.findByHref(policy._links.group.href).pipe( filter(() => this.isActive), getFirstSucceededRemoteDataPayload(), map((group: Group) => group.id) @@ -211,6 +285,21 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { ) } + /** + * Select/unselect all checkbox in the list + */ + selectAllCheckbox(event: any): void { + const checked = event.target.checked; + this.resourcePoliciesEntries$.value.forEach((entry: ResourcePolicyCheckboxEntry) => entry.checked = checked); + } + + /** + * Select/unselect checkbox + */ + selectCheckbox(policyEntry: ResourcePolicyCheckboxEntry, checked: boolean) { + policyEntry.checked = checked; + } + /** * Unsubscribe from all subscriptions */ @@ -220,4 +309,5 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()) } + } From 0b65bdac46b487bffc7130ca658cf3c0ec7c42a4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 6 Apr 2020 20:47:52 +0200 Subject: [PATCH 028/103] fixed lint error --- .../item-authorizations/item-authorizations.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts index 940c5a0ef5..a7d8ae2630 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture } from '@angular/core/testing'; import { ItemAuthorizationsComponent } from './item-authorizations.component'; describe('ItemAuthorizationsComponent', () => { - let comp: ItemAuthorizationsComponent; - let fixture: ComponentFixture; + // let comp: ItemAuthorizationsComponent; + // let fixture: ComponentFixture; }); From 99c1234a7df29faa8e56cdfc297b29b70b4b9834 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 7 Apr 2020 09:26:24 +0200 Subject: [PATCH 029/103] Disable policyType and action fields in resource policy edit form --- .../resource-policies/form/resource-policy-form.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/shared/resource-policies/form/resource-policy-form.ts b/src/app/shared/resource-policies/form/resource-policy-form.ts index b8316112e9..2e14db38ce 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.ts @@ -177,11 +177,19 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { */ private buildResourcePolicyForm(): DynamicFormControlModel[] { const formModel: DynamicFormControlModel[] = []; + // TODO to be removed when https://jira.lyrasis.org/browse/DS-4477 will be implemented + const policyTypeConf = Object.assign({}, RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG, { + disabled: isNotEmpty(this.resourcePolicy) + }); + // TODO to be removed when https://jira.lyrasis.org/browse/DS-4477 will be implemented + const actionConf = Object.assign({}, RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG, { + disabled: isNotEmpty(this.resourcePolicy) + }); formModel.push( new DsDynamicInputModel(RESOURCE_POLICY_FORM_NAME_CONFIG), new DsDynamicTextAreaModel(RESOURCE_POLICY_FORM_DESCRIPTION_CONFIG), - new DynamicSelectModel(RESOURCE_POLICY_FORM_POLICY_TYPE_CONFIG), - new DynamicSelectModel(RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG) + new DynamicSelectModel(policyTypeConf), + new DynamicSelectModel(actionConf) ); const startDateModel = new DynamicDatePickerModel( From 37fe6d21938591b89e23d9576dde0843fcbd84ef Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 7 Apr 2020 09:29:21 +0200 Subject: [PATCH 030/103] Fixed failed test --- src/app/core/resource-policy/resource-policy.service.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/core/resource-policy/resource-policy.service.spec.ts b/src/app/core/resource-policy/resource-policy.service.spec.ts index 78566d61a3..c3f577a8e5 100644 --- a/src/app/core/resource-policy/resource-policy.service.spec.ts +++ b/src/app/core/resource-policy/resource-policy.service.spec.ts @@ -131,6 +131,7 @@ describe('ResourcePolicyService', () => { spyOn((service as any).dataService, 'create').and.callThrough(); spyOn((service as any).dataService, 'delete').and.callThrough(); + spyOn((service as any).dataService, 'update').and.callThrough(); spyOn((service as any).dataService, 'findById').and.callThrough(); spyOn((service as any).dataService, 'findByHref').and.callThrough(); spyOn((service as any).dataService, 'searchBy').and.callThrough(); From e43f04e56487c77d731026b6da854e5a44c5bf32 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 8 Apr 2020 15:41:55 +0200 Subject: [PATCH 031/103] finished tests --- .../admin-workflow-page.component.ts | 2 +- ...arch-result-grid-element.component.spec.ts | 5 + ...ow-search-result-grid-element.component.ts | 8 +- ...in-workflow-grid-element.component.spec.ts | 23 +++- ...m-admin-workflow-grid-element.component.ts | 18 ++- ...arch-result-list-element.component.spec.ts | 5 + ...ow-search-result-list-element.component.ts | 8 +- ...in-workflow-list-element.component.spec.ts | 14 +- ...m-admin-workflow-list-element.component.ts | 9 +- ...w-item-admin-workflow-actions.component.ts | 4 +- ...orkflow-item-action-page.component.spec.ts | 124 ++++++++++++++++++ .../workflow-item-action-page.component.ts | 23 +++- .../workflow-item-delete.component.spec.ts | 52 +++++++- .../workflow-item-delete.component.ts | 10 ++ .../workflow-item-page.resolver.spec.ts | 29 ++++ .../workflow-item-page.resolver.ts | 8 +- .../workflow-item-send-back.component.spec.ts | 54 +++++++- .../workflow-item-send-back.component.ts | 10 ++ 18 files changed, 383 insertions(+), 23 deletions(-) create mode 100644 src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts create mode 100644 src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts index 37ddc70692..8c86c8ec98 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts @@ -8,7 +8,7 @@ import { Context } from '../../core/shared/context.model'; }) /** - * Component that represents a search page for administrators + * Component that represents a workflow item search page for administrators */ export class AdminWorkflowPageComponent { /** diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts index a2d88465d5..a2461bffda 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts @@ -21,6 +21,7 @@ import { SearchResult } from '../../../../../shared/search/search-result.model'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; describe('TaskAdminWorkflowSearchResultGridElementComponent', () => { let component: TaskAdminWorkflowSearchResultGridElementComponent; @@ -79,4 +80,8 @@ describe('TaskAdminWorkflowSearchResultGridElementComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should retrieve the workflow item using the link service', () => { + expect(linkService.resolveLink).toHaveBeenCalledWith(searchResult.indexableObject, followLink('workflowitem')); + }); }); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts index 1f394fe134..cc002c87c0 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts @@ -24,15 +24,21 @@ import { BitstreamDataService } from '../../../../../core/data/bitstream-data.se templateUrl: './task-admin-workflow-search-result-grid-element.component.html' }) /** - * The component for displaying a list element for an pool task search result on the admin search page + * The component for displaying a list element for an task search result on the admin workflow search page */ export class TaskAdminWorkflowSearchResultGridElementComponent extends SearchResultGridElementComponent, TaskObject> implements OnInit { + /** + * The workflow item linked to the task object + */ public wfi$: Observable; constructor(private linkService: LinkService, protected truncatableService: TruncatableService, protected bitstreamService: BitstreamDataService) { super(truncatableService, bitstreamService); } + /** + * Initialize the workflow item + */ ngOnInit(): void { super.ngOnInit(); this.dso = this.linkService.resolveLink(this.dso, followLink('workflowitem')); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts index 917d3770bb..4d064444e5 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts @@ -3,8 +3,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; -import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; -import { SharedModule } from '../../../../../shared/shared.module'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; @@ -13,17 +11,25 @@ import { WorkflowItemAdminWorkflowGridElementComponent } from './workflow-item-a import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { PublicationGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component'; +import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; describe('WorkflowItemAdminWorkflowGridElementComponent', () => { let component: WorkflowItemAdminWorkflowGridElementComponent; let fixture: ComponentFixture; let id; let wfi; + let itemRD$; let linkService; function init() { + itemRD$ = createSuccessfulRemoteDataObject$(new Item()); id = '780b2588-bda5-4112-a1cd-0b15000a5339'; wfi = new WorkflowItem(); + wfi.item = itemRD$; linkService = getMockLinkService(); } @@ -31,18 +37,23 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { init(); TestBed.configureTestingModule( { - declarations: [WorkflowItemAdminWorkflowGridElementComponent], + declarations: [WorkflowItemAdminWorkflowGridElementComponent, PublicationGridElementComponent, ListableObjectDirective], imports: [ NoopAnimationsModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), - SharedModule ], providers: [ { provide: LinkService, useValue: linkService }, + { provide: TruncatableService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA] }) + .overrideComponent(WorkflowItemAdminWorkflowGridElementComponent, { + set: { + entryComponents: [PublicationGridElementComponent] + } + }) .compileComponents(); })); @@ -60,4 +71,8 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should retrieve the item using the link service', () => { + expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item')); + }); }); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts index 2ee9f16f52..2139590e6e 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts @@ -36,12 +36,27 @@ import { take } from 'rxjs/operators'; templateUrl: './workflow-item-admin-workflow-grid-element.component.html' }) /** - * The component for displaying a list element for an workflow item on the admin search page + * The component for displaying a grid element for an workflow item on the admin workflow search page */ export class WorkflowItemAdminWorkflowGridElementComponent extends AbstractListableElementComponent { + /** + * Directive used to render the dynamic component in + */ @ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective; + + /** + * The html child that contains the badges html + */ @ViewChild('badges', { static: true }) badges: ElementRef; + + /** + * The html child that contains the button html + */ @ViewChild('buttons', { static: true }) buttons: ElementRef; + + /** + * The item linked to the workflow item + */ public item$: Observable; constructor(private componentFactoryResolver: ComponentFactoryResolver, private linkService: LinkService) { @@ -50,6 +65,7 @@ export class WorkflowItemAdminWorkflowGridElementComponent extends AbstractLista /** * Setup the dynamic child component + * Initialize the item object from the workflow item */ ngOnInit(): void { this.object = this.linkService.resolveLink(this.object, followLink('item')); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts index 68ffe9e421..f67e653ea4 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts @@ -13,6 +13,7 @@ import { LinkService } from '../../../../../core/cache/builders/link.service'; import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; describe('TaskAdminWorkflowSearchResultListElementComponent', () => { let component: TaskAdminWorkflowSearchResultListElementComponent; @@ -60,4 +61,8 @@ describe('TaskAdminWorkflowSearchResultListElementComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should retrieve the workflow item using the link service', () => { + expect(linkService.resolveLink).toHaveBeenCalledWith(searchResult.indexableObject, followLink('workflowitem')); + }); }); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts index 2de768b747..04951f59eb 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts @@ -23,15 +23,21 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl templateUrl: './task-admin-workflow-search-result-list-element.component.html' }) /** - * The component for displaying a list element for an pool task search result on the admin search page + * The component for displaying a grid element for an task search result on the admin workflow search page */ export class TaskAdminWorkflowSearchResultListElementComponent extends SearchResultListElementComponent, TaskObject> { + /** + * The workflow item linked to the task object + */ public wfi$: Observable; constructor(private linkService: LinkService, protected truncatableService: TruncatableService) { super(truncatableService); } + /** + * Initialize the workflow item + */ ngOnInit(): void { super.ngOnInit(); this.dso = this.linkService.resolveLink(this.dso, followLink('workflowitem')); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts index af9bbbc70f..10a05a8b30 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts @@ -13,16 +13,25 @@ import { WorkflowItem } from '../../../../../core/submission/models/workflowitem import { WorkflowItemAdminWorkflowListElementComponent } from './workflow-item-admin-workflow-list-element.component'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { PublicationGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component'; +import { AdminSidebarSectionComponent } from '../../../../admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; describe('WorkflowItemAdminWorkflowListElementComponent', () => { let component: WorkflowItemAdminWorkflowListElementComponent; let fixture: ComponentFixture; let id; let wfi; + let itemRD$; let linkService; + function init() { + itemRD$ = createSuccessfulRemoteDataObject$(new Item()); id = '780b2588-bda5-4112-a1cd-0b15000a5339'; wfi = new WorkflowItem(); + wfi.item = itemRD$; linkService = getMockLinkService(); } @@ -35,7 +44,6 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => { NoopAnimationsModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), - SharedModule ], providers: [ { provide: TruncatableService, useValue: mockTruncatableService }, @@ -60,4 +68,8 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should retrieve the item using the link service', () => { + expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item')); + }); }); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts index 7681d7e75d..835d6acfbb 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts @@ -18,15 +18,22 @@ import { Item } from '../../../../../core/shared/item.model'; templateUrl: './workflow-item-admin-workflow-list-element.component.html' }) /** - * The component for displaying a list element for an workflow item on the admin search page + * The component for displaying a list element for an workflow item on the admin workflow search page */ export class WorkflowItemAdminWorkflowListElementComponent extends AbstractListableElementComponent implements OnInit { + + /** + * The item linked to the workflow item + */ public item$: Observable; constructor(private linkService: LinkService) { super(); } + /** + * Initialize the item object from the workflow item + */ ngOnInit(): void { this.object = this.linkService.resolveLink(this.object, followLink('item')); this.item$ = (this.object.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts index 58f89e31e6..2109357b81 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts @@ -8,11 +8,11 @@ import { getWorkflowItemDeletePath, getWorkflowItemSendBackPath } from '../../.. templateUrl: './workflow-item-admin-workflow-actions.component.html' }) /** - * The component for displaying the actions for a list element for an item on the admin workflow page + * The component for displaying the actions for a list element for an item on the admin workflow search page */ export class WorkflowItemAdminWorkflowActionsComponent { /** - * The item to perform the actions on + * The workflow item to perform the actions on */ @Input() public wfi: WorkflowItem; diff --git a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts new file mode 100644 index 0000000000..71d9346b29 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts @@ -0,0 +1,124 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { WorkflowItemActionPageComponent } from './workflow-item-action-page.component'; +import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/testing/utils'; +import { ActivatedRouteStub } from '../shared/testing/active-router-stub'; +import { NotificationsServiceStub } from '../shared/testing/notifications-service-stub'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { RouteService } from '../core/services/route.service'; +import { RouterStub } from '../shared/testing/router-stub'; +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { Observable, of as observableOf } from 'rxjs'; +import { VarDirective } from '../shared/utils/var.directive'; +import { By } from '@angular/platform-browser'; + +const type = 'testType'; +describe('WorkflowItemActionPageComponent', () => { + let component: WorkflowItemActionPageComponent; + let fixture: ComponentFixture; + let wfiService; + let wfi; + let itemRD$; + let id; + + function init() { + wfiService = jasmine.createSpyObj('workflowItemService', { + sendBack: observableOf(true) + }); + itemRD$ = createSuccessfulRemoteDataObject$(itemRD$); + wfi = new WorkflowItem(); + wfi.item = itemRD$; + id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80'; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + })], + declarations: [TestComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, + { provide: Router, useClass: RouterStub }, + { provide: RouteService, useValue: {} }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: WorkflowItemDataService, useValue: wfiService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set the initial type correctly', () => { + expect(component.type).toEqual(type); + }); + + describe('clicking the button with class btn-danger', () => { + beforeEach(() => { + spyOn(component, 'performAction'); + }); + + it('should call performAction on clicking the btn-danger', () => { + const button = fixture.debugElement.query(By.css('.btn-danger')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(component.performAction).toHaveBeenCalled(); + }); + }); + + describe('clicking the button with class btn-default', () => { + beforeEach(() => { + spyOn(component, 'previousPage'); + }); + + it('should call performAction on clicking the btn-default', () => { + const button = fixture.debugElement.query(By.css('.btn-default')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(component.previousPage).toHaveBeenCalled(); + }); + }); +}); + +@Component({ + selector: 'ds-workflow-item-test-action-page', + templateUrl: 'workflow-item-action-page.component.html' + } +) +class TestComponent extends WorkflowItemActionPageComponent { + constructor(protected route: ActivatedRoute, + protected workflowItemService: WorkflowItemDataService, + protected router: Router, + protected routeService: RouteService, + protected notificationsService: NotificationsService, + protected translationService: TranslateService) { + super(route, workflowItemService, router, routeService, notificationsService, translationService); + } + + getType(): string { + return type; + } + + sendRequest(id: string): Observable { + return observableOf(true); + } +} diff --git a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts index fcc943a741..2859ca3e44 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts @@ -12,6 +12,9 @@ import { RemoteData } from '../core/data/remote-data'; import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators'; import { isEmpty } from '../shared/empty.util'; +/** + * Abstract component representing a page to perform an action on a workflow item + */ export abstract class WorkflowItemActionPageComponent implements OnInit { public type; public wfi$: Observable; @@ -25,12 +28,18 @@ export abstract class WorkflowItemActionPageComponent implements OnInit { protected translationService: TranslateService) { } + /** + * Sets up the type, workflow item and its item object + */ ngOnInit() { this.type = this.getType(); this.wfi$ = this.route.data.pipe(map((data: Data) => data.wfi as RemoteData), getRemoteDataPayload()); this.item$ = this.wfi$.pipe(switchMap((wfi: WorkflowItem) => (wfi.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); } + /** + * Performs the action and shows a notification based on the outcome of the action + */ performAction() { this.wfi$.pipe( take(1), @@ -49,6 +58,10 @@ export abstract class WorkflowItemActionPageComponent implements OnInit { }) } + /** + * Navigates to the previous url + * If there's not previous url, it continues to the mydspace page instead + */ previousPage() { this.routeService.getPreviousUrl().pipe(take(1)) .subscribe((url) => { @@ -56,10 +69,18 @@ export abstract class WorkflowItemActionPageComponent implements OnInit { url = '/mydspace'; } this.router.navigateByUrl(url); - } ); } + + /** + * Performs the action of this workflow item action page + * @param id The id of the WorkflowItem + */ abstract sendRequest(id: string): Observable; + + /** + * Returns the type of page + */ abstract getType(): string; } diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts index b42fe965c1..d2c093ff4b 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts @@ -1,16 +1,59 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterStub } from '../../shared/testing/router-stub'; +import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; +import { RouteService } from '../../core/services/route.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { of as observableOf } from 'rxjs'; describe('WorkflowItemDeleteComponent', () => { let component: WorkflowItemDeleteComponent; let fixture: ComponentFixture; + let wfiService; + let wfi; + let itemRD$; + let id; + + function init() { + wfiService = jasmine.createSpyObj('workflowItemService', { + delete: observableOf(true) + }); + itemRD$ = createSuccessfulRemoteDataObject$(itemRD$); + wfi = new WorkflowItem(); + wfi.item = itemRD$; + id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80'; + } beforeEach(async(() => { + init(); TestBed.configureTestingModule({ - declarations: [ WorkflowItemDeleteComponent ] + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + })], + declarations: [WorkflowItemDeleteComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, + { provide: Router, useClass: RouterStub }, + { provide: RouteService, useValue: {} }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: WorkflowItemDataService, useValue: wfiService }, + ], + schemas: [NO_ERRORS_SCHEMA] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { @@ -22,4 +65,9 @@ describe('WorkflowItemDeleteComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should call delete on the workflow-item service when sendRequest is called', () => { + component.sendRequest(id); + expect(wfiService.delete).toHaveBeenCalledWith(id); + }); }); diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts index 171aeb27d1..73111bdf2b 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts @@ -11,6 +11,9 @@ import { TranslateService } from '@ngx-translate/core'; selector: 'ds-workflow-item-delete', templateUrl: '../workflow-item-action-page.component.html' }) +/** + * Component representing a page to delete a workflow item + */ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent { constructor(protected route: ActivatedRoute, protected workflowItemService: WorkflowItemDataService, @@ -21,10 +24,17 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent super(route, workflowItemService, router, routeService, notificationsService, translationService); } + /** + * Returns the type of page + */ getType(): string { return 'delete'; } + /** + * Performs the action of this workflow item action page + * @param id The id of the WorkflowItem + */ sendRequest(id: string): Observable { return this.workflowItemService.delete(id); } diff --git a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts new file mode 100644 index 0000000000..792c642ec7 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts @@ -0,0 +1,29 @@ +import { first } from 'rxjs/operators'; +import { of as observableOf } from 'rxjs'; +import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; + +describe('WorkflowItemPageResolver', () => { + describe('resolve', () => { + let resolver: WorkflowItemPageResolver; + let wfiService: WorkflowItemDataService; + const uuid = '1234-65487-12354-1235'; + + beforeEach(() => { + wfiService = { + findById: (id: string) => observableOf({ payload: { id }, hasSucceeded: true }) as any + } as any; + resolver = new WorkflowItemPageResolver(wfiService); + }); + + it('should resolve a workflow item with the correct id', () => { + resolver.resolve({ params: { id: uuid } } as any, undefined) + .pipe(first()) + .subscribe( + (resolved) => { + expect(resolved.payload.id).toEqual(uuid); + } + ); + }); + }); +}); diff --git a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts index 5ae11efff6..19cc4b4914 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts @@ -2,8 +2,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; -import { ItemDataService } from '../core/data/item-data.service'; -import { Item } from '../core/shared/item.model'; import { hasValue } from '../shared/empty.util'; import { find } from 'rxjs/operators'; import { followLink } from '../shared/utils/follow-link-config.model'; @@ -11,7 +9,7 @@ import { WorkflowItemDataService } from '../core/submission/workflowitem-data.se import { WorkflowItem } from '../core/submission/models/workflowitem.model'; /** - * This class represents a resolver that requests a specific item before the route is activated + * This class represents a resolver that requests a specific workflow item before the route is activated */ @Injectable() export class WorkflowItemPageResolver implements Resolve> { @@ -19,10 +17,10 @@ export class WorkflowItemPageResolver implements Resolve> Emits the found item based on the parameters in the current route, + * @returns Observable<> Emits the found workflow item based on the parameters in the current route, * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts index e76e01b7f3..daf58450e4 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts @@ -1,16 +1,59 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterStub } from '../../shared/testing/router-stub'; +import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; +import { RouteService } from '../../core/services/route.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { of as observableOf } from 'rxjs'; +import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; describe('WorkflowItemSendBackComponent', () => { let component: WorkflowItemSendBackComponent; let fixture: ComponentFixture; + let wfiService; + let wfi; + let itemRD$; + let id; + + function init() { + wfiService = jasmine.createSpyObj('workflowItemService', { + sendBack: observableOf(true) + }); + itemRD$ = createSuccessfulRemoteDataObject$(itemRD$); + wfi = new WorkflowItem(); + wfi.item = itemRD$; + id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80'; + } beforeEach(async(() => { + init(); TestBed.configureTestingModule({ - declarations: [ WorkflowItemSendBackComponent ] + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + })], + declarations: [WorkflowItemSendBackComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, + { provide: Router, useClass: RouterStub }, + { provide: RouteService, useValue: {} }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: WorkflowItemDataService, useValue: wfiService }, + ], + schemas: [NO_ERRORS_SCHEMA] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { @@ -22,4 +65,9 @@ describe('WorkflowItemSendBackComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should call sendBack on the workflow-item service when sendRequest is called', () => { + component.sendRequest(id); + expect(wfiService.sendBack).toHaveBeenCalledWith(id); + }); }); diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts index c4006811d9..6e9a2e841e 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts @@ -11,6 +11,9 @@ import { TranslateService } from '@ngx-translate/core'; selector: 'ds-workflow-item-send-back', templateUrl: '../workflow-item-action-page.component.html' }) +/** + * Component representing a page to send back a workflow item to the submitter + */ export class WorkflowItemSendBackComponent extends WorkflowItemActionPageComponent { constructor(protected route: ActivatedRoute, protected workflowItemService: WorkflowItemDataService, @@ -21,10 +24,17 @@ export class WorkflowItemSendBackComponent extends WorkflowItemActionPageCompone super(route, workflowItemService, router, routeService, notificationsService, translationService); } + /** + * Returns the type of page + */ getType(): string { return 'send-back'; } + /** + * Performs the action of this workflow item action page + * @param id The id of the WorkflowItem + */ sendRequest(id: string): Observable { return this.workflowItemService.sendBack(id); } From 9cfed06784db4f7aee998dd9e2d54efb3fc663a0 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 9 Apr 2020 10:20:42 +0200 Subject: [PATCH 032/103] fixed caching issue --- .../workflow-item-admin-workflow-actions.component.ts | 2 ++ .../workflow-item-delete.component.spec.ts | 3 +++ .../workflow-item-delete/workflow-item-delete.component.ts | 5 ++++- .../workflow-item-send-back.component.spec.ts | 3 +++ .../workflow-item-send-back.component.ts | 5 ++++- 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts index 2109357b81..d44f870b14 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts @@ -11,6 +11,7 @@ import { getWorkflowItemDeletePath, getWorkflowItemSendBackPath } from '../../.. * The component for displaying the actions for a list element for an item on the admin workflow search page */ export class WorkflowItemAdminWorkflowActionsComponent { + /** * The workflow item to perform the actions on */ @@ -25,6 +26,7 @@ export class WorkflowItemAdminWorkflowActionsComponent { * Returns the path to the delete page of this workflow item */ getDeletePath(): string { + return getWorkflowItemDeletePath(this.wfi.id) } diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts index d2c093ff4b..c11cdc1d15 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts @@ -15,6 +15,8 @@ import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { VarDirective } from '../../shared/utils/var.directive'; import { of as observableOf } from 'rxjs'; +import { RequestService } from '../../core/data/request.service'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; describe('WorkflowItemDeleteComponent', () => { let component: WorkflowItemDeleteComponent; @@ -50,6 +52,7 @@ describe('WorkflowItemDeleteComponent', () => { { provide: RouteService, useValue: {} }, { provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: WorkflowItemDataService, useValue: wfiService }, + { provide: RequestService, useValue: getMockRequestService() }, ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts index 73111bdf2b..43c3e90152 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts @@ -6,6 +6,7 @@ import { WorkflowItemDataService } from '../../core/submission/workflowitem-data import { RouteService } from '../../core/services/route.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +import { RequestService } from '../../core/data/request.service'; @Component({ selector: 'ds-workflow-item-delete', @@ -20,7 +21,8 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent protected router: Router, protected routeService: RouteService, protected notificationsService: NotificationsService, - protected translationService: TranslateService) { + protected translationService: TranslateService, + protected requestService: RequestService) { super(route, workflowItemService, router, routeService, notificationsService, translationService); } @@ -36,6 +38,7 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent * @param id The id of the WorkflowItem */ sendRequest(id: string): Observable { + this.requestService.removeByHrefSubstring('/discover'); return this.workflowItemService.delete(id); } } diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts index daf58450e4..0332a6b9ee 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts @@ -15,6 +15,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { VarDirective } from '../../shared/utils/var.directive'; import { of as observableOf } from 'rxjs'; import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; +import { RequestService } from '../../core/data/request.service'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; describe('WorkflowItemSendBackComponent', () => { let component: WorkflowItemSendBackComponent; @@ -50,6 +52,7 @@ describe('WorkflowItemSendBackComponent', () => { { provide: RouteService, useValue: {} }, { provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: WorkflowItemDataService, useValue: wfiService }, + { provide: RequestService, useValue: getMockRequestService() }, ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts index 6e9a2e841e..002e5dcc9a 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts @@ -6,6 +6,7 @@ import { WorkflowItemDataService } from '../../core/submission/workflowitem-data import { RouteService } from '../../core/services/route.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +import { RequestService } from '../../core/data/request.service'; @Component({ selector: 'ds-workflow-item-send-back', @@ -20,7 +21,8 @@ export class WorkflowItemSendBackComponent extends WorkflowItemActionPageCompone protected router: Router, protected routeService: RouteService, protected notificationsService: NotificationsService, - protected translationService: TranslateService) { + protected translationService: TranslateService, + protected requestService: RequestService) { super(route, workflowItemService, router, routeService, notificationsService, translationService); } @@ -36,6 +38,7 @@ export class WorkflowItemSendBackComponent extends WorkflowItemActionPageCompone * @param id The id of the WorkflowItem */ sendRequest(id: string): Observable { + this.requestService.removeByHrefSubstring('/discover'); return this.workflowItemService.sendBack(id); } } From 3ac575c3604a4d8ea3eb47024b1c2d0668bbb767 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 9 Apr 2020 11:29:39 +0200 Subject: [PATCH 033/103] fixed messages for Admin Workflow --- resources/i18n/en.json5 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index a515113445..20c7b05ec6 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -263,9 +263,9 @@ - "admin.workflow.breadcrumbs": "Admin Workflow", + "admin.workflow.breadcrumbs": "Administer Workflow", - "admin.workflow.title": "Admin Workflow", + "admin.workflow.title": "Administer Workflow", "admin.workflow.item.workflow": "Workflow", @@ -1399,7 +1399,7 @@ "menu.section.toggle.statistics_task": "Toggle Statistics Task section", - "menu.section.workflow": "Active Workflows", + "menu.section.workflow": "Administer Workflow", "mydspace.description": "", @@ -2232,7 +2232,7 @@ - "workflowAdmin.search.results.head": "Active Workflows", + "workflowAdmin.search.results.head": "Administer Workflow", From 3d482aed26f902b04b76d898d6a7512272e929a1 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 9 Apr 2020 16:17:27 +0200 Subject: [PATCH 034/103] removed unnecessary import --- src/app/+item-page/edit-item-page/edit-item-page.module.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index d02aafcfa1..ac6125fb1c 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -5,7 +5,6 @@ import { EditItemPageRoutingModule } from './edit-item-page.routing.module'; import { EditItemPageComponent } from './edit-item-page.component'; import { ItemStatusComponent } from './item-status/item-status.component'; import { ItemOperationComponent } from './item-operation/item-operation.component'; -import { ModifyItemOverviewComponent } from './modify-item-overview/modify-item-overview.component'; import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component'; import { ItemReinstateComponent } from './item-reinstate/item-reinstate.component'; import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract-simple-item-action.component'; @@ -47,7 +46,6 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version ItemOperationComponent, AbstractSimpleItemActionComponent, AbstractItemUpdateComponent, - ModifyItemOverviewComponent, ItemWithdrawComponent, ItemReinstateComponent, ItemPrivateComponent, From 638793ca5e438d226f8fd2666c157f855ffc087c Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 10 Apr 2020 17:41:39 +0200 Subject: [PATCH 035/103] 70373: Login as EPerson intermediate commit --- resources/i18n/en.json5 | 8 ++++ .../eperson-form/eperson-form.component.html | 12 +++++ .../eperson-form/eperson-form.component.ts | 46 ++++++++++++++++++- src/app/core/auth/auth.interceptor.ts | 12 ++++- src/app/core/auth/auth.service.ts | 43 ++++++++++++++++- src/app/shared/form/form.component.html | 2 + 6 files changed, 118 insertions(+), 5 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index ae3176d8b1..7f9c41c366 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -170,6 +170,14 @@ + "admin.access-control.epeople.actions.delete": "Delete EPerson", + + "admin.access-control.epeople.actions.impersonate": "Impersonate EPerson", + + "admin.access-control.epeople.actions.reset": "Reset password", + + "admin.access-control.epeople.actions.stop-impersonating": "Stop impersonating EPerson", + "admin.access-control.epeople.title": "DSpace Angular :: EPeople", "admin.access-control.epeople.head": "EPeople", diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html index 578862b561..b87b3e0848 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -14,6 +14,18 @@ [formLayout]="formLayout" (cancel)="onCancel()" (submitForm)="onSubmit()"> + + + +
diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts index cbcaef78dc..f886514ded 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -7,7 +7,7 @@ import { DynamicInputModel } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; -import { Subscription, combineLatest } from 'rxjs'; +import { Subscription, combineLatest, of } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; import { take } from 'rxjs/operators'; import { RestResponse } from '../../../../core/cache/response.models'; @@ -22,6 +22,7 @@ import { hasValue } from '../../../../shared/empty.util'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { AuthService } from '../../../../core/auth/auth.service'; @Component({ selector: 'ds-eperson-form', @@ -105,6 +106,24 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ @Output() cancelForm: EventEmitter = new EventEmitter(); + /** + * Observable whether or not the admin is allowed to reset the EPerson's password + * TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false) + */ + canReset$: Observable = of(false); + + /** + * Observable whether or not the admin is allowed to delete the EPerson + * TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false) + */ + canDelete$: Observable = of(false); + + /** + * Observable whether or not the admin is allowed to impersonate the EPerson + * TODO: Initialize the observable once the REST API supports this (currently hardcoded to return true) + */ + canImpersonate$: Observable = of(true); + /** * List of subscriptions */ @@ -129,13 +148,20 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ epersonInitial: EPerson; + /** + * Whether or not this EPerson is currently being impersonated + */ + isImpersonated = false; + constructor(public epersonService: EPersonDataService, public groupsDataService: GroupDataService, private formBuilderService: FormBuilderService, private translateService: TranslateService, - private notificationsService: NotificationsService,) { + private notificationsService: NotificationsService, + private authService: AuthService) { this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { this.epersonInitial = eperson; + this.isImpersonated = this.authService.isImpersonatingUser(eperson.id); })); } @@ -364,6 +390,22 @@ export class EPersonFormComponent implements OnInit, OnDestroy { })); } + /** + * Start impersonating the EPerson + */ + impersonate() { + this.authService.impersonate(this.epersonInitial.id); + this.isImpersonated = true; + } + + /** + * Stop impersonating the EPerson + */ + stopImpersonating() { + this.authService.stopImpersonating(); + this.isImpersonated = false; + } + /** * Cancel the current edit when component is destroyed & unsub all subscriptions */ diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index 6d609a4ea3..bf11d00ccd 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -18,7 +18,7 @@ import { AppState } from '../../app.reducer'; import { AuthService } from './auth.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; +import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions'; import { Store } from '@ngrx/store'; import { Router } from '@angular/router'; @@ -235,8 +235,16 @@ export class AuthInterceptor implements HttpInterceptor { }); // Get the auth header from the service. authorization = authService.buildAuthHeader(token); + let newHeaders = req.headers.set('authorization', authorization); + + // When present, add the ID of the EPerson we're impersonating to the headers + const impersonatingID = authService.getImpersonateID(); + if (hasValue(impersonatingID)) { + newHeaders = newHeaders.set('X-On-Behalf-Of', impersonatingID); + } + // Clone the request to add the new header. - newReq = req.clone({ headers: req.headers.set('authorization', authorization) }); + newReq = req.clone({ headers: newHeaders }); } else { newReq = req.clone(); } diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 0f5c06bbc9..46d02a03cf 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -14,7 +14,7 @@ import { AuthRequestService } from './auth-request.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; -import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; import { CookieService } from '../services/cookie.service'; import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors'; import { AppState, routerStateSelector } from '../../app.reducer'; @@ -33,6 +33,7 @@ import { AuthMethod } from './models/auth.method'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; export const REDIRECT_COOKIE = 'dsRedirectUrl'; +export const IMPERSONATING_COOKIE = 'dsImpersonatingEPerson'; /** * The auth service. @@ -469,4 +470,44 @@ export class AuthService { this.storage.remove(REDIRECT_COOKIE); } + /** + * Start impersonating EPerson + * @param epersonId ID of the EPerson to impersonate + */ + impersonate(epersonId: string) { + this.storage.set(IMPERSONATING_COOKIE, epersonId); + this.refreshAfterLogout(); + } + + /** + * Stop impersonating EPerson + */ + stopImpersonating() { + this.storage.remove(IMPERSONATING_COOKIE); + this.refreshAfterLogout(); + } + + /** + * Get the ID of the EPerson we're currently impersonating + * Returns undefined if we're not impersonating anyone + */ + getImpersonateID(): string { + return this.storage.get(IMPERSONATING_COOKIE); + } + + /** + * Whether or not we are currently impersonating an EPerson + */ + isImpersonating(): boolean { + return hasValue(this.getImpersonateID()); + } + + /** + * Whether or not we are currently impersonating a specific EPerson + * @param epersonId ID of the EPerson to check + */ + isImpersonatingUser(epersonId: string): boolean { + return this.getImpersonateID() === epersonId; + } + } diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 24948680c7..20fb942380 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -45,6 +45,8 @@ + +

From 46498992a729aa1772a3772c0e25251ed3b0fd80 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 14 Apr 2020 20:58:58 +0200 Subject: [PATCH 036/103] Renamed ResourcePolicyFormComponent's file --- ...esource-policy-form.ts => resource-policy-form.component.ts} | 0 src/app/shared/shared.module.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/app/shared/resource-policies/form/{resource-policy-form.ts => resource-policy-form.component.ts} (100%) diff --git a/src/app/shared/resource-policies/form/resource-policy-form.ts b/src/app/shared/resource-policies/form/resource-policy-form.component.ts similarity index 100% rename from src/app/shared/resource-policies/form/resource-policy-form.ts rename to src/app/shared/resource-policies/form/resource-policy-form.component.ts diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c8c4db5692..d914e3d679 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -193,7 +193,7 @@ import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/swi import { ClaimedTaskActionsEditMetadataComponent } from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; import { ResourcePoliciesComponent } from './resource-policies/resource-policies.component'; import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive'; -import { ResourcePolicyFormComponent } from './resource-policies/form/resource-policy-form'; +import { ResourcePolicyFormComponent } from './resource-policies/form/resource-policy-form.component'; import { EpersonGroupListComponent } from './resource-policies/form/eperson-group-list/eperson-group-list.component'; import { ResourcePolicyTargetResolver } from './resource-policies/resolvers/resource-policy-target.resolver'; import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver'; From 00f2aa5e1c3e165e564d1ee79b4efc6e74116d3c Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 14 Apr 2020 21:13:43 +0200 Subject: [PATCH 037/103] Renamed ResourcePolicyFormComponent's file --- ...rce-policy-form.html => resource-policy-form.component.html} | 0 .../resource-policies/form/resource-policy-form.component.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/app/shared/resource-policies/form/{resource-policy-form.html => resource-policy-form.component.html} (100%) diff --git a/src/app/shared/resource-policies/form/resource-policy-form.html b/src/app/shared/resource-policies/form/resource-policy-form.component.html similarity index 100% rename from src/app/shared/resource-policies/form/resource-policy-form.html rename to src/app/shared/resource-policies/form/resource-policy-form.component.html diff --git a/src/app/shared/resource-policies/form/resource-policy-form.component.ts b/src/app/shared/resource-policies/form/resource-policy-form.component.ts index 2e14db38ce..5cb3afc894 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.component.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.component.ts @@ -47,7 +47,7 @@ export interface ResourcePolicyEvent { @Component({ selector: 'ds-resource-policy-form', - templateUrl: './resource-policy-form.html', + templateUrl: './resource-policy-form.component.html', }) /** * Component that show form for adding/editing a resource policy From e43aa15a707800d8ca5d28c5a9f86c061ce32e18 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 15 Apr 2020 11:14:41 +0200 Subject: [PATCH 038/103] 70373: Store authenticated EPerson ID in store instead of object --- src/app/core/auth/auth.actions.ts | 6 +-- src/app/core/auth/auth.effects.ts | 12 +++++- src/app/core/auth/auth.reducer.ts | 11 +++--- src/app/core/auth/auth.service.ts | 37 +++++++++++++++++-- src/app/core/auth/selectors.ts | 16 ++++---- .../profile-page/profile-page.component.ts | 11 ++---- .../auth-nav-menu/auth-nav-menu.component.ts | 9 +++-- .../user-menu/user-menu.component.ts | 8 ++-- 8 files changed, 72 insertions(+), 38 deletions(-) diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index 2c2224e878..9237c30db9 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -402,10 +402,10 @@ export class RetrieveAuthenticatedEpersonAction implements Action { */ export class RetrieveAuthenticatedEpersonSuccessAction implements Action { public type: string = AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS; - payload: EPerson; + payload: string; - constructor(user: EPerson) { - this.payload = user ; + constructor(userId: string) { + this.payload = userId ; } } diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index d153748fb9..35d5e7b043 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -43,6 +43,7 @@ import { RetrieveAuthMethodsSuccessAction, RetrieveTokenAction } from './auth.actions'; +import { hasValue } from '../../shared/empty.util'; @Injectable() export class AuthEffects { @@ -97,8 +98,15 @@ export class AuthEffects { public retrieveAuthenticatedEperson$: Observable = this.actions$.pipe( ofType(AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON), switchMap((action: RetrieveAuthenticatedEpersonAction) => { - return this.authService.retrieveAuthenticatedUserByHref(action.payload).pipe( - map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user)), + const impersonatedUserID = this.authService.getImpersonateID(); + let user$: Observable; + if (hasValue(impersonatedUserID)) { + user$ = this.authService.retrieveAuthenticatedUserById(impersonatedUserID); + } else { + user$ = this.authService.retrieveAuthenticatedUserByHref(action.payload); + } + return user$.pipe( + map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user.id)), catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error)))); }) ); diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 19fd162d3f..16990b35a8 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -14,7 +14,6 @@ import { SetRedirectUrlAction } from './auth.actions'; // import models -import { EPerson } from '../eperson/models/eperson.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; @@ -49,8 +48,8 @@ export interface AuthState { // true when refreshing token refreshing?: boolean; - // the authenticated user - user?: EPerson; + // the authenticated user's id + userId?: string; // all authentication Methods enabled at the backend authMethods?: AuthMethod[]; @@ -112,7 +111,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut error: undefined, loading: false, info: undefined, - user: (action as RetrieveAuthenticatedEpersonSuccessAction).payload + userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload }); case AuthActionTypes.AUTHENTICATE_ERROR: @@ -144,7 +143,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: false, info: undefined, refreshing: false, - user: undefined + userId: undefined }); case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED: @@ -155,7 +154,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loaded: false, loading: false, info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload, - user: undefined + userId: undefined }); case AuthActionTypes.REGISTRATION: diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 46d02a03cf..7d642e60d2 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { Observable, of as observableOf } from 'rxjs'; -import { distinctUntilChanged, filter, map, startWith, take, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLatestFrom } from 'rxjs/operators'; import { RouterReducerState } from '@ngrx/router-store'; import { select, Store } from '@ngrx/store'; import { CookieAttributes } from 'js-cookie'; @@ -14,9 +14,15 @@ import { AuthRequestService } from './auth-request.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; -import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; import { CookieService } from '../services/cookie.service'; -import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors'; +import { + getAuthenticatedUserId, + getAuthenticationToken, + getRedirectUrl, + isAuthenticated, + isTokenRefreshing +} from './selectors'; import { AppState, routerStateSelector } from '../../app.reducer'; import { CheckAuthenticationTokenAction, @@ -164,7 +170,7 @@ export class AuthService { } /** - * Returns the authenticated user + * Returns the authenticated user by href * @returns {User} */ public retrieveAuthenticatedUserByHref(userHref: string): Observable { @@ -173,6 +179,29 @@ export class AuthService { ) } + /** + * Returns the authenticated user by id + * @returns {User} + */ + public retrieveAuthenticatedUserById(userId: string): Observable { + return this.epersonService.findById(userId).pipe( + getAllSucceededRemoteDataPayload() + ) + } + + /** + * Returns the authenticated user from the store + * @returns {User} + */ + public getAuthenticatedUserFromStore(): Observable { + return this.store.pipe( + select(getAuthenticatedUserId), + hasValueOperator(), + switchMap((id: string) => this.epersonService.findById(id)), + getAllSucceededRemoteDataPayload() + ) + } + /** * Checks if token is present into browser storage and is valid. */ diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts index 4e51bc1fc9..173f82e810 100644 --- a/src/app/core/auth/selectors.ts +++ b/src/app/core/auth/selectors.ts @@ -8,7 +8,6 @@ import { createSelector } from '@ngrx/store'; */ import { AuthState } from './auth.reducer'; import { AppState } from '../../app.reducer'; -import { EPerson } from '../eperson/models/eperson.model'; /** * Returns the user state. @@ -36,12 +35,11 @@ const _isAuthenticatedLoaded = (state: AuthState) => state.loaded; /** * Return the users state - * NOTE: when state is REHYDRATED user object lose prototype so return always a new EPerson object - * @function _getAuthenticatedUser + * @function _getAuthenticatedUserId * @param {State} state - * @returns {EPerson} + * @returns {string} User ID */ -const _getAuthenticatedUser = (state: AuthState) => Object.assign(new EPerson(), state.user); +const _getAuthenticatedUserId = (state: AuthState) => state.userId; /** * Returns the authentication error. @@ -119,13 +117,13 @@ const _getAuthenticationMethods = (state: AuthState) => state.authMethods; export const getAuthenticationMethods = createSelector(getAuthState, _getAuthenticationMethods); /** - * Returns the authenticated user - * @function getAuthenticatedUser + * Returns the authenticated user id + * @function getAuthenticatedUserId * @param {AuthState} state * @param {any} props - * @return {User} + * @return {string} User ID */ -export const getAuthenticatedUser = createSelector(getAuthState, _getAuthenticatedUser); +export const getAuthenticatedUserId = createSelector(getAuthState, _getAuthenticatedUserId); /** * Returns the authentication error. diff --git a/src/app/profile-page/profile-page.component.ts b/src/app/profile-page/profile-page.component.ts index 5a2736593a..8f4cd492a2 100644 --- a/src/app/profile-page/profile-page.component.ts +++ b/src/app/profile-page/profile-page.component.ts @@ -1,9 +1,6 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { Observable } from 'rxjs/internal/Observable'; import { EPerson } from '../core/eperson/models/eperson.model'; -import { select, Store } from '@ngrx/store'; -import { getAuthenticatedUser } from '../core/auth/selectors'; -import { AppState } from '../app.reducer'; import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component'; import { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component'; import { NotificationsService } from '../shared/notifications/notifications.service'; @@ -13,9 +10,10 @@ import { RemoteData } from '../core/data/remote-data'; import { PaginatedList } from '../core/data/paginated-list'; import { filter, switchMap, tap } from 'rxjs/operators'; import { EPersonDataService } from '../core/eperson/eperson-data.service'; -import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../core/shared/operators'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators'; import { hasValue } from '../shared/empty.util'; import { followLink } from '../shared/utils/follow-link-config.model'; +import { AuthService } from '../core/auth/auth.service'; @Component({ selector: 'ds-profile-page', @@ -50,15 +48,14 @@ export class ProfilePageComponent implements OnInit { */ NOTIFICATIONS_PREFIX = 'profile.notifications.'; - constructor(private store: Store, + constructor(private authService: AuthService, private notificationsService: NotificationsService, private translate: TranslateService, private epersonService: EPersonDataService) { } ngOnInit(): void { - this.user$ = this.store.pipe( - select(getAuthenticatedUser), + this.user$ = this.authService.getAuthenticatedUserFromStore().pipe( filter((user: EPerson) => hasValue(user.id)), switchMap((user: EPerson) => this.epersonService.findById(user.id, followLink('groups'))), getAllSucceededRemoteData(), diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index 1b39ad15d9..37a45e1b20 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -9,9 +9,9 @@ import { fadeInOut, fadeOut } from '../animations/fade'; import { HostWindowService } from '../host-window.service'; import { AppState, routerStateSelector } from '../../app.reducer'; import { isNotUndefined } from '../empty.util'; -import { getAuthenticatedUser, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors'; +import { isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors'; import { EPerson } from '../../core/eperson/models/eperson.model'; -import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service'; +import { AuthService, LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service'; @Component({ selector: 'ds-auth-nav-menu', @@ -41,7 +41,8 @@ export class AuthNavMenuComponent implements OnInit { public sub: Subscription; constructor(private store: Store, - private windowService: HostWindowService + private windowService: HostWindowService, + private authService: AuthService ) { this.isXsOrSm$ = this.windowService.isXsOrSm(); } @@ -53,7 +54,7 @@ export class AuthNavMenuComponent implements OnInit { // set loading this.loading = this.store.pipe(select(isAuthenticationLoading)); - this.user = this.store.pipe(select(getAuthenticatedUser)); + this.user = this.authService.getAuthenticatedUserFromStore(); this.showAuth = this.store.pipe( select(routerStateSelector), diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts index 2d57a837c7..81d9b3d555 100644 --- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts +++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts @@ -5,9 +5,10 @@ import { select, Store } from '@ngrx/store'; import { EPerson } from '../../../core/eperson/models/eperson.model'; import { AppState } from '../../../app.reducer'; -import { getAuthenticatedUser, isAuthenticationLoading } from '../../../core/auth/selectors'; +import { isAuthenticationLoading } from '../../../core/auth/selectors'; import { MYDSPACE_ROUTE } from '../../../+my-dspace-page/my-dspace-page.component'; import { getProfileModulePath } from '../../../app-routing.module'; +import { AuthService } from '../../../core/auth/auth.service'; /** * This component represents the user nav menu. @@ -42,7 +43,8 @@ export class UserMenuComponent implements OnInit { */ public profileRoute = getProfileModulePath(); - constructor(private store: Store) { + constructor(private store: Store, + private authService: AuthService) { } /** @@ -54,7 +56,7 @@ export class UserMenuComponent implements OnInit { this.loading$ = this.store.pipe(select(isAuthenticationLoading)); // set user - this.user$ = this.store.pipe(select(getAuthenticatedUser)); + this.user$ = this.authService.getAuthenticatedUserFromStore(); } } From c48bb2cbb0ac8f02f92ce98bd2fbb6a3d2cf4213 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 15 Apr 2020 13:08:04 +0200 Subject: [PATCH 039/103] 70373: Stop impersonating button in navbar + clear cookie on logout --- resources/i18n/en.json5 | 2 + .../eperson-form/eperson-form.component.ts | 2 +- src/app/core/auth/auth.effects.ts | 1 + src/app/core/auth/auth.service.ts | 7 ++++ src/app/header/header.component.html | 1 + .../impersonate-navbar.component.html | 7 ++++ .../impersonate-navbar.component.spec.ts | 0 .../impersonate-navbar.component.ts | 42 +++++++++++++++++++ src/app/shared/shared.module.ts | 4 +- 9 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/app/shared/impersonate-navbar/impersonate-navbar.component.html create mode 100644 src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts create mode 100644 src/app/shared/impersonate-navbar/impersonate-navbar.component.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 7f9c41c366..d41f314325 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1754,6 +1754,8 @@ "nav.statistics.header": "Statistics", + "nav.stop-impersonating": "Stop impersonating EPerson", + "orgunit.listelement.badge": "Organizational Unit", diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts index f886514ded..f221042418 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -402,7 +402,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Stop impersonating the EPerson */ stopImpersonating() { - this.authService.stopImpersonating(); + this.authService.stopImpersonatingAndRefresh(); this.isImpersonated = false; } diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 35d5e7b043..717aaff01e 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -201,6 +201,7 @@ export class AuthEffects { .pipe( ofType(AuthActionTypes.LOG_OUT), switchMap(() => { + this.authService.stopImpersonating(); return this.authService.logout().pipe( map((value) => new LogOutSuccessAction()), catchError((error) => observableOf(new LogOutErrorAction(error))) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 7d642e60d2..ce0fafe277 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -513,6 +513,13 @@ export class AuthService { */ stopImpersonating() { this.storage.remove(IMPERSONATING_COOKIE); + } + + /** + * Stop impersonating EPerson and refresh the store/ui + */ + stopImpersonatingAndRefresh() { + this.stopImpersonating(); this.refreshAfterLogout(); } diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 58f7cb1ecf..5ce0cdb410 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -8,6 +8,7 @@ +
+ + diff --git a/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts b/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/impersonate-navbar/impersonate-navbar.component.ts b/src/app/shared/impersonate-navbar/impersonate-navbar.component.ts new file mode 100644 index 0000000000..19293566ef --- /dev/null +++ b/src/app/shared/impersonate-navbar/impersonate-navbar.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '../../app.reducer'; +import { AuthService } from '../../core/auth/auth.service'; +import { Observable } from 'rxjs/internal/Observable'; +import { isAuthenticated } from '../../core/auth/selectors'; + +@Component({ + selector: 'ds-impersonate-navbar', + templateUrl: 'impersonate-navbar.component.html' +}) +/** + * Navbar component for actions to take concerning impersonating users + */ +export class ImpersonateNavbarComponent implements OnInit { + /** + * Whether or not the user is authenticated. + * @type {Observable} + */ + isAuthenticated$: Observable; + + /** + * Is the user currently impersonating another user? + */ + isImpersonating: boolean; + + constructor(private store: Store, + private authService: AuthService) { + } + + ngOnInit(): void { + this.isAuthenticated$ = this.store.pipe(select(isAuthenticated)); + this.isImpersonating = this.authService.isImpersonating(); + } + + /** + * Stop impersonating the user + */ + stopImpersonating() { + this.authService.stopImpersonatingAndRefresh(); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index a136b7826c..ccf4b6d223 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -191,6 +191,7 @@ import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-ve import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component'; import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive'; import { ClaimedTaskActionsEditMetadataComponent } from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; +import { ImpersonateNavbarComponent } from './impersonate-navbar/impersonate-navbar.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -367,7 +368,8 @@ const COMPONENTS = [ LogInContainerComponent, ItemVersionsComponent, PublicationSearchResultListElementComponent, - ItemVersionsNoticeComponent + ItemVersionsNoticeComponent, + ImpersonateNavbarComponent ]; const ENTRY_COMPONENTS = [ From 4cd11bcbcafe2433476cc9c1bf78eac74a16cb26 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 15 Apr 2020 15:35:41 +0200 Subject: [PATCH 040/103] 70373: Fix existing texts --- .../eperson-form.component.spec.ts | 3 ++ .../eperson-form/eperson-form.component.ts | 4 ++- src/app/core/auth/auth.effects.spec.ts | 2 +- src/app/core/auth/auth.reducer.spec.ts | 30 +++++++++---------- .../profile-page.component.spec.ts | 11 +++++-- .../auth-nav-menu.component.spec.ts | 29 ++++++++++-------- .../user-menu/user-menu.component.spec.ts | 21 ++++++++++--- src/app/shared/testing/auth-service-stub.ts | 30 +++++++++++++++++++ 8 files changed, 94 insertions(+), 36 deletions(-) diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index 4319c77e8b..7a301aa7e9 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -31,6 +31,8 @@ import { NotificationsServiceStub } from '../../../../shared/testing/notificatio import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; import { EPeopleRegistryComponent } from '../epeople-registry.component'; import { EPersonFormComponent } from './eperson-form.component'; +import { AuthService } from '../../../../core/auth/auth.service'; +import { AuthServiceStub } from '../../../../shared/testing/auth-service-stub'; describe('EPersonFormComponent', () => { let component: EPersonFormComponent; @@ -125,6 +127,7 @@ describe('EPersonFormComponent', () => { { provide: Store, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, + { provide: AuthService, useValue: new AuthServiceStub() }, EPeopleRegistryComponent ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts index f221042418..9e3bcc88c0 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -161,7 +161,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy { private authService: AuthService) { this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { this.epersonInitial = eperson; - this.isImpersonated = this.authService.isImpersonatingUser(eperson.id); + if (hasValue(eperson)) { + this.isImpersonated = this.authService.isImpersonatingUser(eperson.id); + } })); } diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 1f6fa51afd..872c71022f 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -234,7 +234,7 @@ describe('AuthEffects', () => { } }); - const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock) }); + const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id) }); expect(authEffects.retrieveAuthenticatedEperson$).toBeObservable(expected); }); diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index 7a39ef3da4..a50c469836 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -189,7 +189,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; const action = new LogOutAction(); @@ -206,7 +206,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; const action = new LogOutSuccessAction(); @@ -219,7 +219,7 @@ describe('authReducer', () => { loading: false, info: undefined, refreshing: false, - user: undefined + userId: undefined }; expect(newState).toEqual(state); }); @@ -232,7 +232,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; const action = new LogOutErrorAction(mockError); @@ -244,7 +244,7 @@ describe('authReducer', () => { error: 'Test error message', loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; expect(newState).toEqual(state); }); @@ -258,7 +258,7 @@ describe('authReducer', () => { loading: true, info: undefined }; - const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock); + const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id); const newState = authReducer(initialState, action); state = { authenticated: true, @@ -267,7 +267,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; expect(newState).toEqual(state); }); @@ -301,7 +301,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); const action = new RefreshTokenAction(newTokenInfo); @@ -313,7 +313,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock, + userId: EPersonMock.id, refreshing: true }; expect(newState).toEqual(state); @@ -327,7 +327,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock, + userId: EPersonMock.id, refreshing: true }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); @@ -340,7 +340,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock, + userId: EPersonMock.id, refreshing: false }; expect(newState).toEqual(state); @@ -354,7 +354,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock, + userId: EPersonMock.id, refreshing: true }; const action = new RefreshTokenErrorAction(); @@ -367,7 +367,7 @@ describe('authReducer', () => { loading: false, info: undefined, refreshing: false, - user: undefined + userId: undefined }; expect(newState).toEqual(state); }); @@ -380,7 +380,7 @@ describe('authReducer', () => { error: undefined, loading: false, info: undefined, - user: EPersonMock + userId: EPersonMock.id }; state = { @@ -390,7 +390,7 @@ describe('authReducer', () => { loading: false, error: undefined, info: 'Message', - user: undefined + userId: undefined }; }); diff --git a/src/app/profile-page/profile-page.component.spec.ts b/src/app/profile-page/profile-page.component.spec.ts index 5992012be9..3b9c8e7d6d 100644 --- a/src/app/profile-page/profile-page.component.spec.ts +++ b/src/app/profile-page/profile-page.component.spec.ts @@ -12,12 +12,15 @@ import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model'; import { EPersonDataService } from '../core/eperson/eperson-data.service'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { authReducer } from '../core/auth/auth.reducer'; +import { of } from 'rxjs/internal/observable/of'; +import { AuthService } from '../core/auth/auth.service'; describe('ProfilePageComponent', () => { let component: ProfilePageComponent; let fixture: ComponentFixture; const user = Object.assign(new EPerson(), { + id: 'userId', groups: createSuccessfulRemoteDataObject$(createPaginatedList([])) }); const authState = { @@ -25,9 +28,12 @@ describe('ProfilePageComponent', () => { loaded: true, loading: false, authToken: new AuthTokenInfo('test_token'), - user: user + userId: user.id }; + const authService = jasmine.createSpyObj('authService', { + getAuthenticatedUserFromStore: of(user) + }); const epersonService = jasmine.createSpyObj('epersonService', { findById: createSuccessfulRemoteDataObject$(user) }); @@ -43,7 +49,8 @@ describe('ProfilePageComponent', () => { imports: [StoreModule.forRoot(authReducer), TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ { provide: EPersonDataService, useValue: epersonService }, - { provide: NotificationsService, useValue: notificationsService } + { provide: NotificationsService, useValue: notificationsService }, + { provide: AuthService, useValue: authService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts index 5e01494674..9c20f2be4d 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -14,6 +14,7 @@ import { HostWindowService } from '../host-window.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; import { AuthService } from '../../core/auth/auth.service'; +import { of } from 'rxjs/internal/observable/of'; describe('AuthNavMenuComponent', () => { @@ -25,9 +26,19 @@ describe('AuthNavMenuComponent', () => { let notAuthState: AuthState; let authState: AuthState; + let authService: AuthService; + let routerState = { url: '/home' }; + + function serviceInit() { + authService = jasmine.createSpyObj('authService', { + getAuthenticatedUserFromStore: of(EPersonMock), + setRedirectUrl: {} + }); + } + function init() { notAuthState = { authenticated: false, @@ -39,13 +50,14 @@ describe('AuthNavMenuComponent', () => { loaded: true, loading: false, authToken: new AuthTokenInfo('test_token'), - user: EPersonMock + userId: EPersonMock.id }; } describe('when is a not mobile view', () => { beforeEach(async(() => { const window = new HostWindowServiceStub(800); + serviceInit(); // refine the test module by declaring the test component TestBed.configureTestingModule({ @@ -59,12 +71,7 @@ describe('AuthNavMenuComponent', () => { ], providers: [ { provide: HostWindowService, useValue: window }, - { - provide: AuthService, useValue: { - setRedirectUrl: () => { /*empty*/ - } - } - } + { provide: AuthService, useValue: authService } ], schemas: [ CUSTOM_ELEMENTS_SCHEMA @@ -239,6 +246,7 @@ describe('AuthNavMenuComponent', () => { describe('when is a mobile view', () => { beforeEach(async(() => { const window = new HostWindowServiceStub(300); + serviceInit(); // refine the test module by declaring the test component TestBed.configureTestingModule({ @@ -252,12 +260,7 @@ describe('AuthNavMenuComponent', () => { ], providers: [ { provide: HostWindowService, useValue: window }, - { - provide: AuthService, useValue: { - setRedirectUrl: () => { /*empty*/ - } - } - } + { provide: AuthService, useValue: authService } ], schemas: [ CUSTOM_ELEMENTS_SCHEMA diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts index 512d9e0917..80d03dfc47 100644 --- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts @@ -12,6 +12,8 @@ import { AppState } from '../../../app.reducer'; import { MockTranslateLoader } from '../../mocks/mock-translate-loader'; import { cold } from 'jasmine-marbles'; import { By } from '@angular/platform-browser'; +import { AuthService } from '../../../core/auth/auth.service'; +import { of } from 'rxjs'; describe('UserMenuComponent', () => { @@ -20,6 +22,13 @@ describe('UserMenuComponent', () => { let deUserMenu: DebugElement; let authState: AuthState; let authStateLoading: AuthState; + let authService: AuthService; + + function serviceInit() { + authService = jasmine.createSpyObj('authService', { + getAuthenticatedUserFromStore: of(EPersonMock) + }); + } function init() { authState = { @@ -27,18 +36,19 @@ describe('UserMenuComponent', () => { loaded: true, loading: false, authToken: new AuthTokenInfo('test_token'), - user: EPersonMock + userId: EPersonMock.id }; authStateLoading = { authenticated: true, loaded: true, loading: true, authToken: null, - user: EPersonMock + userId: EPersonMock.id }; } beforeEach(async(() => { + serviceInit(); TestBed.configureTestingModule({ imports: [ StoreModule.forRoot(authReducer), @@ -49,6 +59,9 @@ describe('UserMenuComponent', () => { } }) ], + providers: [ + { provide: AuthService, useValue: authService } + ], declarations: [ UserMenuComponent ], @@ -93,7 +106,7 @@ describe('UserMenuComponent', () => { b: true })); - expect(component.user$).toBeObservable(cold('c', { + expect(component.user$).toBeObservable(cold('(c|)', { c: EPersonMock })); @@ -132,7 +145,7 @@ describe('UserMenuComponent', () => { b: false })); - expect(component.user$).toBeObservable(cold('c', { + expect(component.user$).toBeObservable(cold('(c|)', { c: EPersonMock })); diff --git a/src/app/shared/testing/auth-service-stub.ts b/src/app/shared/testing/auth-service-stub.ts index 26ce79cb5f..2fd401aa49 100644 --- a/src/app/shared/testing/auth-service-stub.ts +++ b/src/app/shared/testing/auth-service-stub.ts @@ -5,6 +5,7 @@ import { EPersonMock } from './eperson-mock'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { createSuccessfulRemoteDataObject$ } from './utils'; import { AuthMethod } from '../../core/auth/models/auth.method'; +import { hasValue } from '../empty.util'; export const authMethodsMock = [ new AuthMethod('password'), @@ -14,6 +15,7 @@ export const authMethodsMock = [ export class AuthServiceStub { token: AuthTokenInfo = new AuthTokenInfo('token_test'); + impersonating: string; private _tokenExpired = false; private redirectUrl; @@ -47,6 +49,10 @@ export class AuthServiceStub { return observableOf(EPersonMock); } + public retrieveAuthenticatedUserById(id: string): Observable { + return observableOf(EPersonMock); + } + public buildAuthHeader(token?: AuthTokenInfo): string { return `Bearer ${token.accessToken}`; } @@ -120,4 +126,28 @@ export class AuthServiceStub { retrieveAuthMethodsFromAuthStatus(status: AuthStatus) { return observableOf(authMethodsMock); } + + impersonate(id: string) { + this.impersonating = id; + } + + isImpersonating() { + return hasValue(this.impersonating); + } + + isImpersonatingUser(id: string) { + return this.impersonating === id; + } + + stopImpersonating() { + this.impersonating = undefined; + } + + stopImpersonatingAndRefresh() { + this.stopImpersonating(); + } + + getImpersonateID() { + return this.impersonating; + } } From e7d0c96a2666b9c37306e0e1538d709ddbc1a2b2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:11:24 +0200 Subject: [PATCH 041/103] Added test for ItemAuthorizationsComponent --- .../item-authorizations.component.spec.ts | 196 +++++++++++++++++- .../item-authorizations.component.ts | 17 +- 2 files changed, 204 insertions(+), 9 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts index a7d8ae2630..5447b09167 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts @@ -1,9 +1,197 @@ -import { ComponentFixture } from '@angular/core/testing'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { of as observableOf } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; +import { cold } from 'jasmine-marbles'; import { ItemAuthorizationsComponent } from './item-authorizations.component'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { createMockRDPaginatedObs } from '../item-bitstreams/item-bitstreams.component.spec'; +import { Item } from '../../../core/shared/item.model'; +import { LinkService } from '../../../core/cache/builders/link.service'; +import { getMockLinkService } from '../../../shared/mocks/mock-link-service'; +import { createSuccessfulRemoteDataObject, createTestComponent } from '../../../shared/testing/utils'; +import { getMockResourcePolicyService } from '../../../shared/mocks/mock-resource-policy-service'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PageInfo } from '../../../core/shared/page-info.model'; -describe('ItemAuthorizationsComponent', () => { - // let comp: ItemAuthorizationsComponent; - // let fixture: ComponentFixture; +describe('ItemAuthorizationsComponent test suite', () => { + let comp: ItemAuthorizationsComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let de; + let routerStub: any; + const resourcePolicyService: any = getMockResourcePolicyService(); + const linkService: any = getMockLinkService(); + const bitstream1 = Object.assign(new Bitstream(), { + id: 'bitstream1', + uuid: 'bitstream1' + }); + const bitstream2 = Object.assign(new Bitstream(), { + id: 'bitstream2', + uuid: 'bitstream2' + }); + const bitstream3 = Object.assign(new Bitstream(), { + id: 'bitstream3', + uuid: 'bitstream3' + }); + const bitstream4 = Object.assign(new Bitstream(), { + id: 'bitstream4', + uuid: 'bitstream4' + }); + const bundle1 = Object.assign(new Bundle(), { + id: 'bundle1', + uuid: 'bundle1', + _links: { + self: { href: 'bundle1-selflink' } + }, + bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2]) + }); + const bundle2 = Object.assign(new Bundle(), { + id: 'bundle2', + uuid: 'bundle2', + _links: { + self: { href: 'bundle2-selflink' } + }, + bitstreams: createMockRDPaginatedObs([bitstream3, bitstream4]) + }); + const bundles = [bundle1, bundle2]; + const bitstreamList1: PaginatedList = new PaginatedList(new PageInfo(), [bitstream1, bitstream2]); + const bitstreamList2: PaginatedList = new PaginatedList(new PageInfo(), [bitstream3, bitstream4]); + + const item = Object.assign(new Item(), { + uuid: 'item', + id: 'item', + _links: { + self: { href: 'item-selflink' } + }, + bundles: createMockRDPaginatedObs([bundle1, bundle2]) + }); + + const routeStub = { + data: observableOf({ + item: createSuccessfulRemoteDataObject(item) + }) + }; + + const epersonService = jasmine.createSpyObj('epersonService', { + findByHref: jasmine.createSpy('findByHref'), + }); + + const groupService = jasmine.createSpyObj('groupService', { + findByHref: jasmine.createSpy('findByHref'), + }); + + routerStub = Object.assign(new RouterStub(), { + url: `url/edit` + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + TranslateModule.forRoot() + ], + declarations: [ + ItemAuthorizationsComponent, + TestComponent + ], + providers: [ + { provide: LinkService, useValue: linkService }, + { provide: ActivatedRoute, useValue: routeStub }, + ItemAuthorizationsComponent + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create ItemAuthorizationsComponent', inject([ItemAuthorizationsComponent], (app: ItemAuthorizationsComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(ItemAuthorizationsComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + linkService.resolveLink.and.callFake((object, link) => object); + fixture.detectChanges(); + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should init bundles and bitstreams map properly', () => { + expect(compAsAny.subs.length).toBe(2); + expect(compAsAny.bundles$.value).toEqual(bundles); + expect(compAsAny.bundleBitstreamsMap.has('bundle1')).toBeTruthy(); + expect(compAsAny.bundleBitstreamsMap.has('bundle2')).toBeTruthy(); + let bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle1'); + expect(bitstreamList).toBeObservable(cold('(a|)', { + a: bitstreamList1 + })); + + bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle2'); + expect(bitstreamList).toBeObservable(cold('(a|)', { + a: bitstreamList2 + })); + }); + + it('should get the item UUID', () => { + + expect(comp.getItemUUID()).toBeObservable(cold('(a|)', { + a: item.id + })); + + }); + + it('should get the item\'s bundle', () => { + + expect(comp.getItemBundles()).toBeObservable(cold('a', { + a: bundles + })); + + }); + }); }); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index 3322d4cc36..8153990a02 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -4,7 +4,6 @@ import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; import { catchError, filter, first, flatMap, map, take } from 'rxjs/operators'; -import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; import { PaginatedList } from '../../../core/data/paginated-list'; import { getFirstSucceededRemoteDataPayload, @@ -35,6 +34,10 @@ interface BundleBitstreamsMapEntry { */ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { + /** + * A map that contains all bitstream of the item's bundles + * @type {Observable>>>} + */ public bundleBitstreamsMap: Map>> = new Map>>(); /** @@ -59,12 +62,10 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * Initialize instance variables * * @param {LinkService} linkService - * @param {ResourcePolicyService} resourcePolicyService * @param {ActivatedRoute} route */ constructor( private linkService: LinkService, - private resourcePolicyService: ResourcePolicyService, private route: ActivatedRoute ) { } @@ -86,7 +87,10 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { filter((item: Item) => isNotEmpty(item.bundles)), flatMap((item: Item) => item.bundles), getFirstSucceededRemoteDataWithNotEmptyPayload(), - catchError(() => observableOf(new PaginatedList(null, []))) + catchError((error) => { + console.error(error); + return observableOf(new PaginatedList(null, [])) + }) ); this.subs.push( @@ -133,7 +137,10 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { private getBundleBitstreams(bundle: Bundle): Observable> { return bundle.bitstreams.pipe( getFirstSucceededRemoteDataPayload(), - catchError(() => observableOf(new PaginatedList(null, []))) + catchError((error) => { + console.error(error); + return observableOf(new PaginatedList(null, [])) + }) ) } From ee38de488c9726fa02282bb2e856f0b27f164a15 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:12:05 +0200 Subject: [PATCH 042/103] Added test for ResourcePoliciesComponent --- .../mocks/mock-resource-policy-service.ts | 10 + .../resource-policies.component.spec.ts | 469 ++++++++++++++++++ .../resource-policies.component.ts | 71 ++- src/app/shared/testing/group-mock.ts | 1 + 4 files changed, 529 insertions(+), 22 deletions(-) create mode 100644 src/app/shared/mocks/mock-resource-policy-service.ts create mode 100644 src/app/shared/resource-policies/resource-policies.component.spec.ts diff --git a/src/app/shared/mocks/mock-resource-policy-service.ts b/src/app/shared/mocks/mock-resource-policy-service.ts new file mode 100644 index 0000000000..864cf20730 --- /dev/null +++ b/src/app/shared/mocks/mock-resource-policy-service.ts @@ -0,0 +1,10 @@ +import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service'; + +export function getMockResourcePolicyService(): ResourcePolicyService { + return jasmine.createSpyObj('resourcePolicyService', { + searchByResource: jasmine.createSpy('searchByResource'), + create: jasmine.createSpy('create'), + delete: jasmine.createSpy('delete'), + update: jasmine.createSpy('update') + }); +} diff --git a/src/app/shared/resource-policies/resource-policies.component.spec.ts b/src/app/shared/resource-policies/resource-policies.component.spec.ts new file mode 100644 index 0000000000..4e318ca630 --- /dev/null +++ b/src/app/shared/resource-policies/resource-policies.component.spec.ts @@ -0,0 +1,469 @@ +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; + +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; + +import { Bitstream } from '../../core/shared/bitstream.model'; +import { Bundle } from '../../core/shared/bundle.model'; +import { createMockRDPaginatedObs } from '../../+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec'; +import { Item } from '../../core/shared/item.model'; +import { LinkService } from '../../core/cache/builders/link.service'; +import { getMockLinkService } from '../mocks/mock-link-service'; +import { createSuccessfulRemoteDataObject, createTestComponent } from '../testing/utils'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationsServiceStub } from '../testing/notifications-service-stub'; +import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service'; +import { getMockResourcePolicyService } from '../mocks/mock-resource-policy-service'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { RequestService } from '../../core/data/request.service'; +import { getMockRequestService } from '../mocks/mock-request.service'; +import { RouterStub } from '../testing/router-stub'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { ResourcePoliciesComponent } from './resource-policies.component'; +import { PolicyType } from '../../core/resource-policy/models/policy-type.model'; +import { ActionType } from '../../core/resource-policy/models/action-type.model'; +import { EPersonMock } from '../testing/eperson-mock'; +import { GroupMock } from '../testing/group-mock'; + +describe('ResourcePoliciesComponent test suite', () => { + let comp: ResourcePoliciesComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let de; + let routerStub: any; + let scheduler: TestScheduler; + const notificationsServiceStub = new NotificationsServiceStub(); + const resourcePolicyService: any = getMockResourcePolicyService(); + const linkService: any = getMockLinkService(); + + const resourcePolicy: any = { + id: '1', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.READ, + startDate: null, + endDate: null, + type: 'resourcepolicy', + uuid: 'resource-policy-1', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + }, + eperson: observableOf(createSuccessfulRemoteDataObject({})), + group: observableOf(createSuccessfulRemoteDataObject(GroupMock)) + }; + + const anotherResourcePolicy: any = { + id: '2', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.WRITE, + startDate: null, + endDate: null, + type: 'resourcepolicy', + uuid: 'resource-policy-2', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + }, + eperson: observableOf(createSuccessfulRemoteDataObject(EPersonMock)), + group: observableOf(createSuccessfulRemoteDataObject({})) + }; + + const bitstream1 = Object.assign(new Bitstream(), { + id: 'bitstream1', + uuid: 'bitstream1' + }); + const bitstream2 = Object.assign(new Bitstream(), { + id: 'bitstream2', + uuid: 'bitstream2' + }); + const bitstream3 = Object.assign(new Bitstream(), { + id: 'bitstream3', + uuid: 'bitstream3' + }); + const bitstream4 = Object.assign(new Bitstream(), { + id: 'bitstream4', + uuid: 'bitstream4' + }); + const bundle1 = Object.assign(new Bundle(), { + id: 'bundle1', + uuid: 'bundle1', + _links: { + self: { href: 'bundle1-selflink' } + }, + bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2]) + }); + const bundle2 = Object.assign(new Bundle(), { + id: 'bundle2', + uuid: 'bundle2', + _links: { + self: { href: 'bundle2-selflink' } + }, + bitstreams: createMockRDPaginatedObs([bitstream3, bitstream4]) + }); + + const item = Object.assign(new Item(), { + uuid: 'itemUUID', + id: 'itemUUID', + _links: { + self: { href: 'item-selflink' } + }, + bundles: createMockRDPaginatedObs([bundle1, bundle2]) + }); + + const routeStub = { + data: observableOf({ + item: createSuccessfulRemoteDataObject(item) + }) + }; + + const epersonService = jasmine.createSpyObj('epersonService', { + findByHref: jasmine.createSpy('findByHref'), + }); + + const groupService = jasmine.createSpyObj('groupService', { + findByHref: jasmine.createSpy('findByHref'), + }); + + routerStub = Object.assign(new RouterStub(), { + url: `url/edit` + }); + + const resourcePolicyEntries = [ + { + id: resourcePolicy.id, + policy: resourcePolicy, + checked: false + }, + { + id: anotherResourcePolicy.id, + policy: anotherResourcePolicy, + checked: false + } + ]; + const resourcePolicySelectedEntries = [ + { + id: resourcePolicy.id, + policy: resourcePolicy, + checked: true + }, + { + id: anotherResourcePolicy.id, + policy: anotherResourcePolicy, + checked: false + } + ]; + + const pageInfo = new PageInfo(); + const array = [resourcePolicy, anotherResourcePolicy]; + const paginatedList = new PaginatedList(pageInfo, array); + const resourcePolicyRD = createSuccessfulRemoteDataObject(resourcePolicy); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ResourcePoliciesComponent, + TestComponent + ], + providers: [ + { provide: LinkService, useValue: linkService }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: EPersonDataService, useValue: epersonService }, + { provide: GroupDataService, useValue: groupService }, + { provide: NotificationsService, useValue: notificationsServiceStub }, + { provide: ResourcePolicyService, useValue: resourcePolicyService }, + { provide: RequestService, useValue: getMockRequestService() }, + { provide: Router, useValue: routerStub }, + ChangeDetectorRef, + ResourcePoliciesComponent + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create ResourcePoliciesComponent', inject([ResourcePoliciesComponent], (app: ResourcePoliciesComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(ResourcePoliciesComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + linkService.resolveLink.and.callFake((object, link) => object); + + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should init component properly', () => { + spyOn(comp, 'initResourcePolicyLIst'); + fixture.detectChanges(); + expect(compAsAny.isActive).toBeTruthy(); + expect(comp.initResourcePolicyLIst).toHaveBeenCalled(); + }); + + it('should init resource policies list properly', () => { + compAsAny.isActive = true; + resourcePolicyService.searchByResource.and.returnValue(hot('a|', { + a: paginatedListRD + })); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.initResourcePolicyLIst()); + scheduler.flush(); + + expect(compAsAny.resourcePoliciesEntries$.value).toEqual(resourcePolicyEntries); + }); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(ResourcePoliciesComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + linkService.resolveLink.and.callFake((object, link) => object); + compAsAny.isActive = true; + compAsAny.resourcePoliciesEntries$.next(resourcePolicyEntries); + resourcePolicyService.searchByResource.and.returnValue(observableOf({})); + spyOn(comp, 'initResourcePolicyLIst').and.callFake(() => ({})); + fixture.detectChanges(); + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should render a table with a row for each policy', () => { + const rows = fixture.debugElement.queryAll(By.css('table > tbody > tr')); + expect(rows.length).toBe(2); + }); + + describe('canDelete', () => { + it('should return false when no row is selected', () => { + expect(comp.canDelete()).toBeObservable(cold('(a|)', { + a: false + })); + }); + + it('should return true when al least is selected', () => { + const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input')); + + const event = { target: { checked: true } }; + checkbox.triggerEventHandler('change', event); + expect(comp.canDelete()).toBeObservable(cold('(a|)', { + a: true + })); + }); + }); + + describe('deleteSelectedResourcePolicies', () => { + beforeEach(() => { + compAsAny.resourcePoliciesEntries$.next(resourcePolicySelectedEntries); + fixture.detectChanges(); + }); + + it('should notify success when delete is successful', () => { + + resourcePolicyService.delete.and.returnValue(observableOf(true)); + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.deleteSelectedResourcePolicies()); + scheduler.flush(); + + expect(notificationsServiceStub.success).toHaveBeenCalled(); + expect(comp.initResourcePolicyLIst).toHaveBeenCalled(); + }); + + it('should notify error when delete is not successful', () => { + + resourcePolicyService.delete.and.returnValue(observableOf(false)); + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.deleteSelectedResourcePolicies()); + scheduler.flush(); + + expect(notificationsServiceStub.error).toHaveBeenCalled(); + expect(comp.initResourcePolicyLIst).toHaveBeenCalled(); + }); + }); + + it('should get the resource\'s policy list', () => { + + expect(comp.getResourcePolicies()).toBeObservable(cold('a', { + a: resourcePolicyEntries + })); + + }); + + describe('hasEPerson', () => { + it('should true when policy is link to the eperson', () => { + + expect(comp.hasEPerson(anotherResourcePolicy)).toBeObservable(cold('(ab|)', { + a: false, + b: true + })); + + }); + + it('should false when policy is not link to the eperson', () => { + + expect(comp.hasEPerson(resourcePolicy)).toBeObservable(cold('(aa|)', { + a: false + })); + + }); + }); + + describe('hasGroup', () => { + it('should true when policy is link to the group', () => { + + expect(comp.hasGroup(resourcePolicy)).toBeObservable(cold('(ab|)', { + a: false, + b: true + })); + + }); + + it('should false when policy is not link to the group', () => { + + expect(comp.hasGroup(anotherResourcePolicy)).toBeObservable(cold('(aa|)', { + a: false + })); + + }); + }); + + describe('getEPersonName', () => { + it('should return the eperson name', () => { + + expect(comp.getEPersonName(anotherResourcePolicy)).toBeObservable(cold('(ab|)', { + a: '', + b: 'User Test' + })); + }); + }); + + describe('getGroupName', () => { + it('should return the group name', () => { + + expect(comp.getGroupName(resourcePolicy)).toBeObservable(cold('(ab|)', { + a: '', + b: 'testgroupname' + })); + }); + }); + + it('should format date properly', () => { + expect(comp.formatDate('2020-04-14T12:00:00Z')).toBe('2020-04-14'); + }); + + it('should select All Checkbox', () => { + spyOn(comp, 'selectAllCheckbox').and.callThrough(); + const checkbox = fixture.debugElement.query(By.css('table > thead > tr:nth-child(2) input')); + + const event = { target: { checked: true } }; + checkbox.triggerEventHandler('change', event); + expect(comp.selectAllCheckbox).toHaveBeenCalled(); + }); + + it('should select a Checkbox', () => { + spyOn(comp, 'selectCheckbox').and.callThrough(); + const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input')); + + const event = { target: { checked: true } }; + checkbox.triggerEventHandler('change', event); + expect(comp.selectCheckbox).toHaveBeenCalled(); + }); + + it('should redirect to create resource policy page', () => { + + comp.redirectToResourcePolicyCreatePage(); + expect(compAsAny.router.navigate).toHaveBeenCalled(); + }); + + it('should redirect to resource policy edit page', () => { + + comp.redirectToResourcePolicyEditPage(resourcePolicy); + expect(compAsAny.router.navigate).toHaveBeenCalled(); + }); + + it('should redirect to resource policy edit page', () => { + compAsAny.groupService.findByHref.and.returnValue(observableOf(createSuccessfulRemoteDataObject(GroupMock))); + + comp.redirectToGroupEditPage(resourcePolicy); + expect(compAsAny.router.navigate).toHaveBeenCalled(); + }); + }); +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + + resourceUUID = 'itemUUID'; + resourceType = 'item'; +} diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts index 8ad11315a4..76b23c3001 100644 --- a/src/app/shared/resource-policies/resource-policies.component.ts +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -21,6 +21,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { RequestService } from '../../core/data/request.service'; import { NotificationsService } from '../notifications/notifications.service'; import { dateToString, stringToNgbDateStruct } from '../date.util'; +import { followLink } from '../utils/follow-link-config.model'; interface ResourcePolicyCheckboxEntry { id: string; @@ -111,6 +112,11 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { this.initResourcePolicyLIst(); } + /** + * Check if there are any selected resource's policies to be deleted + * + * @return {Observable} + */ canDelete(): Observable { return observableFrom(this.resourcePoliciesEntries$.value).pipe( filter((entry: ResourcePolicyCheckboxEntry) => entry.checked), @@ -120,25 +126,30 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { ) } + /** + * Delete the selected resource's policies + */ deleteSelectedResourcePolicies(): void { this.processingDelete$.next(true); const policiesToDelete: ResourcePolicyCheckboxEntry[] = this.resourcePoliciesEntries$.value .filter((entry: ResourcePolicyCheckboxEntry) => entry.checked); - observableFrom(policiesToDelete).pipe( - concatMap((entry: ResourcePolicyCheckboxEntry) => this.resourcePolicyService.delete(entry.policy.id)), - scan((acc: any, value: any) => [...acc, ...value], []), - filter((results: boolean[]) => results.length === policiesToDelete.length), - take(1), - ).subscribe((results: boolean[]) => { - const failureResults = results.filter((result: boolean) => !result); - if (isEmpty(failureResults)) { - this.notificationsService.success(null, this.translate.get('resource-policies.delete.success.content')); - } else { - this.notificationsService.error(null, this.translate.get('resource-policies.delete.failure.content')); - } - this.initResourcePolicyLIst(); - this.processingDelete$.next(false); - }) + this.subs.push( + observableFrom(policiesToDelete).pipe( + concatMap((entry: ResourcePolicyCheckboxEntry) => this.resourcePolicyService.delete(entry.policy.id)), + scan((acc: any, value: any) => [...acc, ...value], []), + filter((results: boolean[]) => results.length === policiesToDelete.length), + take(1), + ).subscribe((results: boolean[]) => { + const failureResults = results.filter((result: boolean) => !result); + if (isEmpty(failureResults)) { + this.notificationsService.success(null, this.translate.get('resource-policies.delete.success.content')); + } else { + this.notificationsService.error(null, this.translate.get('resource-policies.delete.failure.content')); + } + this.initResourcePolicyLIst(); + this.processingDelete$.next(false); + }) + ) } /** @@ -158,7 +169,8 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ getEPersonName(policy: ResourcePolicy): Observable { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved - return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + // return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + return policy.eperson.pipe( filter(() => this.isActive), getFirstSucceededRemoteDataWithNotEmptyPayload(), map((eperson: EPerson) => this.dsoNameService.getName(eperson)), @@ -173,7 +185,8 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ getGroupName(policy: ResourcePolicy): Observable { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved - return this.groupService.findByHref(policy._links.group.href).pipe( + // return this.groupService.findByHref(policy._links.group.href).pipe( + return policy.group.pipe( filter(() => this.isActive), getFirstSucceededRemoteDataWithNotEmptyPayload(), map((group: Group) => this.dsoNameService.getName(group)), @@ -198,10 +211,12 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ hasEPerson(policy): Observable { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved - return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + // return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + return policy.eperson.pipe( filter(() => this.isActive), getFirstSucceededRemoteDataPayload(), - map((eperson: EPerson) => isNotEmpty(eperson)) + map((eperson: EPerson) => isNotEmpty(eperson)), + startWith(false) ) } @@ -213,17 +228,23 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ hasGroup(policy): Observable { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved - return this.groupService.findByHref(policy._links.group.href).pipe( + // return this.groupService.findByHref(policy._links.group.href).pipe( + return policy.group.pipe( filter(() => this.isActive), getFirstSucceededRemoteDataPayload(), - map((group: Group) => isNotEmpty(group)) + map((group: Group) => isNotEmpty(group)), + startWith(false) ) } + /** + * Initialize the resource's policies list + */ initResourcePolicyLIst() { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved this.requestService.removeByHrefSubstring(this.resourceUUID); - this.resourcePolicyService.searchByResource(this.resourceUUID).pipe( + this.resourcePolicyService.searchByResource(this.resourceUUID, null, + followLink('eperson'), followLink('group')).pipe( filter(() => this.isActive), getSucceededRemoteData(), take(1) @@ -238,6 +259,11 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { }); } + /** + * Return a boolean representing if a delete operation is pending + * + * @return {Observable} + */ isProcessingDelete(): Observable { return this.processingDelete$.asObservable(); } @@ -305,6 +331,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.isActive = false; + this.resourcePoliciesEntries$ = null; this.subs .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()) diff --git a/src/app/shared/testing/group-mock.ts b/src/app/shared/testing/group-mock.ts index 0c9abb4b7d..a0686bc6d0 100644 --- a/src/app/shared/testing/group-mock.ts +++ b/src/app/shared/testing/group-mock.ts @@ -10,6 +10,7 @@ export const GroupMock: Group = Object.assign(new Group(), { }, groups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups' } }, + _name: 'testgroupname', id: 'testgroupid', uuid: 'testgroupid', type: 'group', From ceaa14e9b269d64cc119457758e6bd8c49ee0d56 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:12:35 +0200 Subject: [PATCH 043/103] Added test for ResourcePolicyFormComponent --- .../resource-policy-form.component.spec.ts | 426 ++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts diff --git a/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts b/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts new file mode 100644 index 0000000000..46b80070b1 --- /dev/null +++ b/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts @@ -0,0 +1,426 @@ +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { BrowserModule, By } from '@angular/platform-browser'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { delay } from 'rxjs/operators'; +import { TranslateModule } from '@ngx-translate/core'; + +import { createSuccessfulRemoteDataObject, createTestComponent } from '../../testing/utils'; +import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { RequestService } from '../../../core/data/request.service'; +import { getMockRequestService } from '../../mocks/mock-request.service'; +import { PolicyType } from '../../../core/resource-policy/models/policy-type.model'; +import { ActionType } from '../../../core/resource-policy/models/action-type.model'; +import { GroupMock } from '../../testing/group-mock'; +import { ResourcePolicyEvent, ResourcePolicyFormComponent } from './resource-policy-form.component'; +import { FormService } from '../../form/form.service'; +import { getMockFormService } from '../../mocks/mock-form-service'; +import { FormBuilderService } from '../../form/builder/form-builder.service'; +import { EpersonGroupListComponent } from './eperson-group-list/eperson-group-list.component'; +import { FormComponent } from '../../form/form.component'; +import { stringToNgbDateStruct } from '../../date.util'; +import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; +import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type'; +import { EPersonMock } from '../../testing/eperson-mock'; + +export const mockResourcePolicyFormData = { + name: [ + { + value: 'name', + language: null, + authority: null, + display: 'name', + confidence: -1, + place: 0, + otherInformation: null + } + ], + description: [ + { + value: 'description', + language: null, + authority: null, + display: 'description', + confidence: -1, + place: 0, + otherInformation: null + } + ], + policyType: [ + { + value: 'TYPE_WORKFLOW', + language: null, + authority: null, + display: 'TYPE_WORKFLOW', + confidence: -1, + place: 0, + otherInformation: null + } + ], + action: [ + { + value: 'WRITE', + language: null, + authority: null, + display: 'WRITE', + confidence: -1, + place: 0, + otherInformation: null + } + ], + date: { + start: [ + { + value: { year: '2019', month: '04', day: '14' }, + language: null, + authority: null, + display: '2019-04-14', + confidence: -1, + place: 0, + otherInformation: null + } + ], + end: [ + { + value: { year: '2020', month: '04', day: '14' }, + language: null, + authority: null, + display: '2020-04-14', + confidence: -1, + place: 0, + otherInformation: null + } + ], + } +}; + +export const submittedResourcePolicy = Object.assign(new ResourcePolicy(), { + name: 'name', + description: 'description', + policyType: PolicyType.TYPE_WORKFLOW, + action: ActionType.WRITE, + startDate: '2019-04-14T00:00:00Z', + endDate: '2020-04-14T00:00:00Z', + type: RESOURCE_POLICY +}); + +describe('ResourcePolicyFormComponent test suite', () => { + let comp: ResourcePolicyFormComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let de; + let scheduler: TestScheduler; + + const resourcePolicy: any = { + id: '1', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.READ, + startDate: '2019-04-14', + endDate: '2020-04-14', + type: 'resourcepolicy', + uuid: 'resource-policy-1', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + }, + eperson: observableOf(createSuccessfulRemoteDataObject({})), + group: observableOf(createSuccessfulRemoteDataObject(GroupMock)) + }; + + const epersonService = jasmine.createSpyObj('epersonService', { + findByHref: jasmine.createSpy('findByHref'), + findAll: jasmine.createSpy('findAll') + }); + + const groupService = jasmine.createSpyObj('groupService', { + findByHref: jasmine.createSpy('findByHref'), + findAll: jasmine.createSpy('findAll') + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + FormComponent, + EpersonGroupListComponent, + ResourcePolicyFormComponent, + TestComponent + ], + providers: [ + { provide: EPersonDataService, useValue: epersonService }, + { provide: FormService, useValue: getMockFormService() }, + { provide: GroupDataService, useValue: groupService }, + { provide: RequestService, useValue: getMockRequestService() }, + FormBuilderService, + ChangeDetectorRef, + ResourcePolicyFormComponent + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create ResourcePolicyFormComponent', inject([ResourcePolicyFormComponent], (app: ResourcePolicyFormComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('when resource policy is not provided', () => { + + beforeEach(() => { + // initTestScheduler(); + fixture = TestBed.createComponent(ResourcePolicyFormComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + comp.isProcessing = observableOf(false); + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should init form model properly', () => { + spyOn(compAsAny, 'isFormValid').and.returnValue(observableOf(false)); + spyOn(compAsAny, 'initModelsValue').and.callThrough(); + spyOn(compAsAny, 'buildResourcePolicyForm').and.callThrough(); + fixture.detectChanges(); + + expect(compAsAny.buildResourcePolicyForm).toHaveBeenCalled(); + expect(compAsAny.initModelsValue).toHaveBeenCalled(); + expect(compAsAny.formModel.length).toBe(5); + expect(compAsAny.subs.length).toBe(0); + + }); + + it('should can set grant', () => { + expect(comp.canSetGrant()).toBeTruthy(); + }); + + it('should not have a target name', () => { + expect(comp.getResourcePolicyTargetName()).toBe(''); + }); + + it('should emit reset event', () => { + spyOn(compAsAny.reset, 'emit'); + comp.onReset(); + expect(compAsAny.reset.emit).toHaveBeenCalled(); + }); + + it('should update resource policy grant object properly', () => { + comp.updateObjectSelected(EPersonMock, true); + + expect(comp.resourcePolicyGrant).toEqual(EPersonMock); + expect(comp.resourcePolicyGrantType).toBe('eperson'); + + comp.updateObjectSelected(GroupMock, false); + + expect(comp.resourcePolicyGrant).toEqual(GroupMock); + expect(comp.resourcePolicyGrantType).toBe('group'); + }); + + }); + + describe('when resource policy is provided', () => { + + beforeEach(() => { + // initTestScheduler(); + fixture = TestBed.createComponent(ResourcePolicyFormComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + comp.resourcePolicy = resourcePolicy; + comp.isProcessing = observableOf(false); + compAsAny.ePersonService.findByHref.and.returnValue( + observableOf(createSuccessfulRemoteDataObject({})).pipe(delay(100)) + ); + compAsAny.groupService.findByHref.and.returnValue(observableOf(createSuccessfulRemoteDataObject(GroupMock))); + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should init form model properly', () => { + spyOn(compAsAny, 'isFormValid').and.returnValue(observableOf(false)); + spyOn(compAsAny, 'initModelsValue').and.callThrough(); + spyOn(compAsAny, 'buildResourcePolicyForm').and.callThrough(); + fixture.detectChanges(); + + expect(compAsAny.buildResourcePolicyForm).toHaveBeenCalled(); + expect(compAsAny.initModelsValue).toHaveBeenCalled(); + expect(compAsAny.formModel.length).toBe(5); + expect(compAsAny.subs.length).toBe(1); + expect(compAsAny.formModel[2].value).toBe('TYPE_SUBMISSION'); + expect(compAsAny.formModel[3].value).toBe('READ'); + expect(compAsAny.formModel[4].get(0).value).toEqual(stringToNgbDateStruct('2019-04-14')); + expect(compAsAny.formModel[4].get(1).value).toEqual(stringToNgbDateStruct('2020-04-14')); + + }); + + it('should init resourcePolicyGrant properly', () => { + compAsAny.isActive = true; + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.ngOnInit()); + scheduler.flush(); + + expect(compAsAny.resourcePolicyGrant).toEqual(GroupMock); + }); + + it('should not can set grant', () => { + expect(comp.canSetGrant()).toBeFalsy(); + }); + + it('should have a target name', () => { + compAsAny.resourcePolicyGrant = GroupMock; + + expect(comp.getResourcePolicyTargetName()).toBe('testgroupname'); + }); + + }); + + describe('when form is valid', () => { + beforeEach(() => { + + fixture = TestBed.createComponent(ResourcePolicyFormComponent); + comp = fixture.componentInstance; + compAsAny = comp; + comp.resourcePolicy = resourcePolicy; + comp.isProcessing = observableOf(false); + compAsAny.ePersonService.findByHref.and.returnValue( + observableOf(createSuccessfulRemoteDataObject({})).pipe(delay(100)) + ); + compAsAny.groupService.findByHref.and.returnValue(observableOf(createSuccessfulRemoteDataObject(GroupMock))); + compAsAny.formService.isValid.and.returnValue(observableOf(true)); + compAsAny.isActive = true; + comp.resourcePolicyGrant = GroupMock; + comp.resourcePolicyGrantType = 'group'; + fixture.detectChanges(); + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should not have submit button disabled when submission is valid', () => { + + const depositBtn: any = fixture.debugElement.query(By.css('.btn-primary')); + + expect(depositBtn.nativeElement.disabled).toBeFalsy(); + }); + + it('should emit submit event', () => { + spyOn(compAsAny.submit, 'emit'); + spyOn(compAsAny, 'createResourcePolicyByFormData').and.callThrough(); + compAsAny.formService.getFormData.and.returnValue(observableOf(mockResourcePolicyFormData)); + const eventPayload: ResourcePolicyEvent = Object.create({}); + eventPayload.object = submittedResourcePolicy; + eventPayload.target = { + type: 'group', + uuid: GroupMock.id + }; + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.onSubmit()); + + scheduler.flush(); + + expect(compAsAny.submit.emit).toHaveBeenCalledWith(eventPayload); + expect(compAsAny.createResourcePolicyByFormData).toHaveBeenCalled(); + }); + + }); + + describe('when form is not valid', () => { + beforeEach(() => { + + fixture = TestBed.createComponent(ResourcePolicyFormComponent); + comp = fixture.componentInstance; + compAsAny = comp; + comp.resourcePolicy = resourcePolicy; + comp.isProcessing = observableOf(false); + compAsAny.ePersonService.findByHref.and.returnValue( + observableOf(createSuccessfulRemoteDataObject({})).pipe(delay(100)) + ); + compAsAny.groupService.findByHref.and.returnValue(observableOf(createSuccessfulRemoteDataObject(GroupMock))); + compAsAny.formService.isValid.and.returnValue(observableOf(false)); + compAsAny.isActive = true; + fixture.detectChanges(); + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should have submit button disabled when submission is valid', () => { + + const depositBtn: any = fixture.debugElement.query(By.css('.btn-primary')); + + expect(depositBtn.nativeElement.disabled).toBeTruthy(); + }); + + }); +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + + resourcePolicy = null; + isProcessing = observableOf(false); +} From 83a1f9d31d1d21843b8648b6fae7d16fd0376b43 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:13:06 +0200 Subject: [PATCH 044/103] Added test for EpersonGroupListComponent --- .../eperson-group-list.component.spec.ts | 274 ++++++++++++++++++ .../eperson-group-list.component.ts | 8 +- 2 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts new file mode 100644 index 0000000000..db590696f6 --- /dev/null +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts @@ -0,0 +1,274 @@ +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { ChangeDetectorRef, Component, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; + +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { uniqueId } from 'lodash'; + +import { createSuccessfulRemoteDataObject, createTestComponent } from '../../../testing/utils'; +import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { RequestService } from '../../../../core/data/request.service'; +import { getMockRequestService } from '../../../mocks/mock-request.service'; +import { EpersonGroupListComponent } from './eperson-group-list.component'; +import { EPersonMock } from '../../../testing/eperson-mock'; +import { GroupMock } from '../../../testing/group-mock'; +import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; + +describe('EpersonGroupListComponent test suite', () => { + let comp: EpersonGroupListComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let de; + let scheduler: TestScheduler; + + const paginationOptions: PaginationComponentOptions = new PaginationComponentOptions() + paginationOptions.id = uniqueId('eperson-group-list-pagination-test'); + paginationOptions.pageSize = 5; + + const epersonService = jasmine.createSpyObj('epersonService', + { + findByHref: jasmine.createSpy('findByHref'), + findAll: jasmine.createSpy('findAll'), + }, + { + linkPath: 'epersons' + } + ); + + const groupService = jasmine.createSpyObj('groupService', + { + findByHref: jasmine.createSpy('findByHref'), + findAll: jasmine.createSpy('findAll'), + }, + { + linkPath: 'groups' + } + ); + + const epersonPaginatedList = new PaginatedList(new PageInfo(), [EPersonMock, EPersonMock]); + const epersonPaginatedListRD = createSuccessfulRemoteDataObject(epersonPaginatedList); + + const groupPaginatedList = new PaginatedList(new PageInfo(), [GroupMock, GroupMock]); + const groupPaginatedListRD = createSuccessfulRemoteDataObject(groupPaginatedList); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], + declarations: [ + EpersonGroupListComponent, + TestComponent + ], + providers: [ + { provide: EPersonDataService, useValue: epersonService }, + { provide: GroupDataService, useValue: groupService }, + { provide: RequestService, useValue: getMockRequestService() }, + EpersonGroupListComponent, + ChangeDetectorRef, + Injector + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create EpersonGroupListComponent', inject([EpersonGroupListComponent], (app: EpersonGroupListComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('when is list of eperson', () => { + + beforeEach(() => { + // initTestScheduler(); + fixture = TestBed.createComponent(EpersonGroupListComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + comp.isListOfEPerson = true; + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should inject EPersonDataService', () => { + spyOn(comp, 'updateList'); + fixture.detectChanges(); + + expect(compAsAny.dataService).toBeDefined(); + expect(comp.updateList).toHaveBeenCalled(); + }); + + it('should init entrySelectedId', () => { + spyOn(comp, 'updateList'); + comp.initSelected = EPersonMock.id; + + fixture.detectChanges(); + + expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id) + }); + + it('should init the list of eperson', () => { + compAsAny.dataService.findAll.and.returnValue(observableOf(epersonPaginatedListRD)); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.updateList(paginationOptions)); + scheduler.flush(); + + expect(compAsAny.list$.value).toEqual(epersonPaginatedListRD); + expect(comp.getList()).toBeObservable(cold('a', { + a: epersonPaginatedListRD + })); + }); + + it('should emit select event', () => { + spyOn(comp.select, 'emit'); + comp.emitSelect(EPersonMock); + + expect(comp.select.emit).toHaveBeenCalled(); + expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id); + }); + + it('should return true when entry is selected', () => { + compAsAny.entrySelectedId.next(EPersonMock.id); + + expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { + a: true + })); + }); + + it('should return false when entry is not selected', () => { + compAsAny.entrySelectedId.next(''); + + expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { + a: false + })); + }); + + it('should update list on page change', () => { + spyOn(comp, 'updateList'); + comp.onPageChange(2); + + expect(compAsAny.updateList).toHaveBeenCalled(); + }); + }); + + describe('when is list of group', () => { + + beforeEach(() => { + // initTestScheduler(); + fixture = TestBed.createComponent(EpersonGroupListComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + comp.isListOfEPerson = false; + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should inject GroupDataService', () => { + spyOn(comp, 'updateList'); + fixture.detectChanges(); + + expect(compAsAny.dataService).toBeDefined(); + expect(comp.updateList).toHaveBeenCalled(); + }); + + it('should init entrySelectedId', () => { + spyOn(comp, 'updateList'); + comp.initSelected = GroupMock.id; + + fixture.detectChanges(); + + expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id) + }); + + it('should init the list of group', () => { + compAsAny.dataService.findAll.and.returnValue(observableOf(groupPaginatedListRD)); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.updateList(paginationOptions)); + scheduler.flush(); + + expect(compAsAny.list$.value).toEqual(groupPaginatedListRD); + expect(comp.getList()).toBeObservable(cold('a', { + a: groupPaginatedListRD + })); + }); + + it('should emit select event', () => { + spyOn(comp.select, 'emit'); + comp.emitSelect(GroupMock); + + expect(comp.select.emit).toHaveBeenCalled(); + expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id); + }); + + it('should return true when entry is selected', () => { + compAsAny.entrySelectedId.next(EPersonMock.id); + + expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { + a: true + })); + }); + + it('should return false when entry is not selected', () => { + compAsAny.entrySelectedId.next(''); + + expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { + a: false + })); + }); + + it('should update list on page change', () => { + spyOn(comp, 'updateList'); + comp.onPageChange(2); + + expect(compAsAny.updateList).toHaveBeenCalled(); + }); + }); +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + + isListOfEPerson = true; + initSelected = ''; +} diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts index b9e1259501..fd033d8728 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts @@ -52,7 +52,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { * The data service used to make request. * It could be EPersonDataService or GroupDataService */ - private dataService: DataService; + private readonly dataService: DataService; /** * A list of eperson or group @@ -71,6 +71,12 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { */ private subs: Subscription[] = []; + /** + * Initialize instance variables and inject the properly DataService + * + * @param {DSONameService} dsoNameService + * @param {Injector} parentInjector + */ constructor(public dsoNameService: DSONameService, private parentInjector: Injector) { const resourceType: ResourceType = (this.isListOfEPerson) ? EPERSON : GROUP; const provider = getDataServiceFor(resourceType); From 4ac3eb5f9bb870c1fbdf4e6f689a1a3b45189ef6 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:13:22 +0200 Subject: [PATCH 045/103] Added test for ResourcePolicyCreateComponent --- .../resource-policy-create.component.spec.ts | 265 ++++++++++++++++++ .../resource-policy-create.component.ts | 31 +- 2 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 src/app/shared/resource-policies/create/resource-policy-create.component.spec.ts diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.spec.ts b/src/app/shared/resource-policies/create/resource-policy-create.component.spec.ts new file mode 100644 index 0000000000..6db1c93da1 --- /dev/null +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.spec.ts @@ -0,0 +1,265 @@ +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { ChangeDetectorRef, Component, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { TranslateModule } from '@ngx-translate/core'; + +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, + createTestComponent +} from '../../testing/utils'; +import { ResourcePolicyCreateComponent } from './resource-policy-create.component'; +import { LinkService } from '../../../core/cache/builders/link.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; +import { getMockResourcePolicyService } from '../../mocks/mock-resource-policy-service'; +import { getMockLinkService } from '../../mocks/mock-link-service'; +import { RouterStub } from '../../testing/router-stub'; +import { Item } from '../../../core/shared/item.model'; +import { createMockRDPaginatedObs } from '../../../+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec'; +import { ResourcePolicyEvent } from '../form/resource-policy-form.component'; +import { GroupMock } from '../../testing/group-mock'; +import { submittedResourcePolicy } from '../form/resource-policy-form.component.spec'; +import { PolicyType } from '../../../core/resource-policy/models/policy-type.model'; +import { ActionType } from '../../../core/resource-policy/models/action-type.model'; +import { EPersonMock } from '../../testing/eperson-mock'; + +describe('ResourcePolicyCreateComponent test suite', () => { + let comp: ResourcePolicyCreateComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let de; + let scheduler: TestScheduler; + let eventPayload: ResourcePolicyEvent; + + const resourcePolicy: any = { + id: '1', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.READ, + startDate: null, + endDate: null, + type: 'resourcepolicy', + uuid: 'resource-policy-1', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + }, + eperson: observableOf(createSuccessfulRemoteDataObject({})), + group: observableOf(createSuccessfulRemoteDataObject(GroupMock)) + }; + + const item = Object.assign(new Item(), { + uuid: 'itemUUID', + id: 'itemUUID', + metadata: { + 'dc.title': [{ + value: 'test item' + }] + }, + _links: { + self: { href: 'item-selflink' } + }, + bundles: createMockRDPaginatedObs([]) + }); + + const resourcePolicyService: any = getMockResourcePolicyService(); + const linkService: any = getMockLinkService(); + const routeStub = { + data: observableOf({ + resourcePolicyTarget: createSuccessfulRemoteDataObject(item) + }) + }; + const routerStub = Object.assign(new RouterStub(), { + url: `url/edit` + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], + declarations: [ + ResourcePolicyCreateComponent, + TestComponent + ], + providers: [ + { provide: LinkService, useValue: linkService }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: ResourcePolicyService, useValue: resourcePolicyService }, + { provide: Router, useValue: routerStub }, + ResourcePolicyCreateComponent, + ChangeDetectorRef, + Injector + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create ResourcePolicyCreateComponent', inject([ResourcePolicyCreateComponent], (app: ResourcePolicyCreateComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('', () => { + + beforeEach(() => { + // initTestScheduler(); + fixture = TestBed.createComponent(ResourcePolicyCreateComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should init component properly', () => { + fixture.detectChanges(); + expect(compAsAny.targetResourceUUID).toBe('itemUUID'); + expect(compAsAny.targetResourceName).toBe('test item'); + }); + + it('should redirect to authorizations page', () => { + comp.redirectToAuthorizationsPage(); + expect(compAsAny.router.navigate).toHaveBeenCalled(); + }); + + it('should return true when is Processing', () => { + compAsAny.processing$.next(true); + expect(comp.isProcessing()).toBeObservable(cold('a', { + a: true + })); + }); + + it('should return false when is not Processing', () => { + compAsAny.processing$.next(false); + expect(comp.isProcessing()).toBeObservable(cold('a', { + a: false + })); + }); + + describe('when target type is group', () => { + beforeEach(() => { + spyOn(comp, 'redirectToAuthorizationsPage').and.callThrough(); + + compAsAny.targetResourceUUID = 'itemUUID'; + + eventPayload = Object.create({}); + eventPayload.object = submittedResourcePolicy; + eventPayload.target = { + type: 'group', + uuid: GroupMock.id + }; + }); + + it('should notify success when creation is successful', () => { + compAsAny.resourcePolicyService.create.and.returnValue(observableOf(createSuccessfulRemoteDataObject(resourcePolicy))); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.createResourcePolicy(eventPayload)); + scheduler.flush(); + + expect(compAsAny.resourcePolicyService.create).toHaveBeenCalledWith(eventPayload.object, 'itemUUID', null, eventPayload.target.uuid); + expect(comp.redirectToAuthorizationsPage).toHaveBeenCalled(); + }); + + it('should notify error when creation is not successful', () => { + compAsAny.resourcePolicyService.create.and.returnValue(observableOf(createFailedRemoteDataObject({}))); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.createResourcePolicy(eventPayload)); + scheduler.flush(); + + expect(compAsAny.resourcePolicyService.create).toHaveBeenCalledWith(eventPayload.object, 'itemUUID', null, eventPayload.target.uuid); + expect(comp.redirectToAuthorizationsPage).not.toHaveBeenCalled(); + }); + }); + + describe('when target type of created policy is eperson', () => { + + beforeEach(() => { + spyOn(comp, 'redirectToAuthorizationsPage').and.callThrough(); + + compAsAny.targetResourceUUID = 'itemUUID'; + + eventPayload = Object.create({}); + eventPayload.object = submittedResourcePolicy; + eventPayload.target = { + type: 'eperson', + uuid: EPersonMock.id + }; + }); + + it('should notify success when creation is successful', () => { + compAsAny.resourcePolicyService.create.and.returnValue(observableOf(createSuccessfulRemoteDataObject(resourcePolicy))); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.createResourcePolicy(eventPayload)); + scheduler.flush(); + + expect(compAsAny.resourcePolicyService.create).toHaveBeenCalledWith(eventPayload.object, 'itemUUID', eventPayload.target.uuid); + expect(comp.redirectToAuthorizationsPage).toHaveBeenCalled(); + }); + + it('should notify error when creation is not successful', () => { + compAsAny.resourcePolicyService.create.and.returnValue(observableOf(createFailedRemoteDataObject({}))); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.createResourcePolicy(eventPayload)); + scheduler.flush(); + + expect(compAsAny.resourcePolicyService.create).toHaveBeenCalledWith(eventPayload.object, 'itemUUID', eventPayload.target.uuid); + expect(comp.redirectToAuthorizationsPage).not.toHaveBeenCalled(); + }); + }); + }); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.ts b/src/app/shared/resource-policies/create/resource-policy-create.component.ts index 4785e39222..e96533515c 100644 --- a/src/app/shared/resource-policies/create/resource-policy-create.component.ts +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.ts @@ -10,7 +10,7 @@ import { ResourcePolicyService } from '../../../core/resource-policy/resource-po import { NotificationsService } from '../../notifications/notifications.service'; import { RemoteData } from '../../../core/data/remote-data'; import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; -import { ResourcePolicyEvent } from '../form/resource-policy-form'; +import { ResourcePolicyEvent } from '../form/resource-policy-form.component'; import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; @@ -36,6 +36,16 @@ export class ResourcePolicyCreateComponent implements OnInit { */ private targetResourceUUID: string; + /** + * Initialize instance variables + * + * @param {DSONameService} dsoNameService + * @param {NotificationsService} notificationsService + * @param {ResourcePolicyService} resourcePolicyService + * @param {ActivatedRoute} route + * @param {Router} router + * @param {TranslateService} translate + */ constructor( private dsoNameService: DSONameService, private notificationsService: NotificationsService, @@ -45,6 +55,9 @@ export class ResourcePolicyCreateComponent implements OnInit { private translate: TranslateService) { } + /** + * Initialize the component + */ ngOnInit(): void { this.route.data.pipe( map((data) => data), @@ -55,14 +68,27 @@ export class ResourcePolicyCreateComponent implements OnInit { }); } + /** + * Return a boolean representing if an operation is pending + * + * @return {Observable} + */ isProcessing(): Observable { return this.processing$.asObservable(); } + /** + * Redirect to the authorizations page + */ redirectToAuthorizationsPage(): void { this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route }); } + /** + * Create a new resource policy + * + * @param event The {{ResourcePolicyEvent}} emitted + */ createResourcePolicy(event: ResourcePolicyEvent): void { this.processing$.next(true); let response$; @@ -79,9 +105,8 @@ export class ResourcePolicyCreateComponent implements OnInit { this.notificationsService.success(null, this.translate.get('resource-policies.create.page.success.content')); this.redirectToAuthorizationsPage(); } else { - this.notificationsService.success(null, this.translate.get('resource-policies.create.page.failure.content')); + this.notificationsService.error(null, this.translate.get('resource-policies.create.page.failure.content')); } }) } - } From b81d8ed900f5ae0c41398a19e78519b8e2e15534 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:13:38 +0200 Subject: [PATCH 046/103] Added test for ResourcePolicyEditComponent --- .../resource-policy-edit.component.spec.ts | 220 ++++++++++++++++++ .../edit/resource-policy-edit.component.ts | 27 ++- 2 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/resource-policies/edit/resource-policy-edit.component.spec.ts diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.spec.ts b/src/app/shared/resource-policies/edit/resource-policy-edit.component.spec.ts new file mode 100644 index 0000000000..56099aac3d --- /dev/null +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.spec.ts @@ -0,0 +1,220 @@ +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { ChangeDetectorRef, Component, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { TranslateModule } from '@ngx-translate/core'; + +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, + createTestComponent +} from '../../testing/utils'; +import { LinkService } from '../../../core/cache/builders/link.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; +import { getMockResourcePolicyService } from '../../mocks/mock-resource-policy-service'; +import { getMockLinkService } from '../../mocks/mock-link-service'; +import { RouterStub } from '../../testing/router-stub'; +import { ResourcePolicyEvent } from '../form/resource-policy-form.component'; +import { GroupMock } from '../../testing/group-mock'; +import { submittedResourcePolicy } from '../form/resource-policy-form.component.spec'; +import { PolicyType } from '../../../core/resource-policy/models/policy-type.model'; +import { ActionType } from '../../../core/resource-policy/models/action-type.model'; +import { ResourcePolicyEditComponent } from './resource-policy-edit.component'; +import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type'; + +describe('ResourcePolicyEditComponent test suite', () => { + let comp: ResourcePolicyEditComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let de; + let scheduler: TestScheduler; + let eventPayload: ResourcePolicyEvent; + let updatedObject; + + const resourcePolicy: any = { + id: '1', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.READ, + startDate: null, + endDate: null, + type: 'resourcepolicy', + uuid: 'resource-policy-1', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + }, + eperson: observableOf(createSuccessfulRemoteDataObject({})), + group: observableOf(createSuccessfulRemoteDataObject(GroupMock)) + }; + + const resourcePolicyService: any = getMockResourcePolicyService(); + const linkService: any = getMockLinkService(); + const routeStub = { + data: observableOf({ + resourcePolicy: createSuccessfulRemoteDataObject(resourcePolicy) + }) + }; + const routerStub = Object.assign(new RouterStub(), { + url: `url/edit` + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], + declarations: [ + ResourcePolicyEditComponent, + TestComponent + ], + providers: [ + { provide: LinkService, useValue: linkService }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: ResourcePolicyService, useValue: resourcePolicyService }, + { provide: Router, useValue: routerStub }, + ResourcePolicyEditComponent, + ChangeDetectorRef, + Injector + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create ResourcePolicyEditComponent', inject([ResourcePolicyEditComponent], (app: ResourcePolicyEditComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('', () => { + + beforeEach(() => { + // initTestScheduler(); + fixture = TestBed.createComponent(ResourcePolicyEditComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should init component properly', () => { + fixture.detectChanges(); + expect(compAsAny.resourcePolicy).toEqual(resourcePolicy); + }); + + it('should redirect to authorizations page', () => { + comp.redirectToAuthorizationsPage(); + expect(compAsAny.router.navigate).toHaveBeenCalled(); + }); + + it('should return true when is Processing', () => { + compAsAny.processing$.next(true); + expect(comp.isProcessing()).toBeObservable(cold('a', { + a: true + })); + }); + + it('should return false when is not Processing', () => { + compAsAny.processing$.next(false); + expect(comp.isProcessing()).toBeObservable(cold('a', { + a: false + })); + }); + + describe('', () => { + beforeEach(() => { + spyOn(comp, 'redirectToAuthorizationsPage').and.callThrough(); + compAsAny.resourcePolicyService.update.and.returnValue(observableOf(createSuccessfulRemoteDataObject(resourcePolicy))); + + compAsAny.targetResourceUUID = 'itemUUID'; + + eventPayload = Object.create({}); + eventPayload.object = submittedResourcePolicy; + eventPayload.target = { + type: 'group', + uuid: GroupMock.id + }; + + compAsAny.resourcePolicy = resourcePolicy; + + updatedObject = Object.assign({}, submittedResourcePolicy, { + id: resourcePolicy.id, + type: RESOURCE_POLICY.value, + _links: resourcePolicy._links + }); + }); + + it('should notify success when update is successful', () => { + compAsAny.resourcePolicyService.update.and.returnValue(observableOf(createSuccessfulRemoteDataObject(resourcePolicy))); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.updateResourcePolicy(eventPayload)); + scheduler.flush(); + + expect(compAsAny.resourcePolicyService.update).toHaveBeenCalledWith(updatedObject); + expect(comp.redirectToAuthorizationsPage).toHaveBeenCalled(); + }); + + it('should notify error when update is not successful', () => { + compAsAny.resourcePolicyService.update.and.returnValue(observableOf(createFailedRemoteDataObject({}))); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.updateResourcePolicy(eventPayload)); + scheduler.flush(); + + expect(compAsAny.resourcePolicyService.update).toHaveBeenCalledWith(updatedObject); + expect(comp.redirectToAuthorizationsPage).not.toHaveBeenCalled(); + }); + }); + }); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts index 20f2a5a34e..e3927e7fcd 100644 --- a/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts @@ -9,7 +9,7 @@ import { ResourcePolicyService } from '../../../core/resource-policy/resource-po import { NotificationsService } from '../../notifications/notifications.service'; import { RemoteData } from '../../../core/data/remote-data'; import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; -import { ResourcePolicyEvent } from '../form/resource-policy-form'; +import { ResourcePolicyEvent } from '../form/resource-policy-form.component'; import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type'; @@ -30,6 +30,15 @@ export class ResourcePolicyEditComponent implements OnInit { */ private processing$ = new BehaviorSubject(false); + /** + * Initialize instance variables + * + * @param {NotificationsService} notificationsService + * @param {ResourcePolicyService} resourcePolicyService + * @param {ActivatedRoute} route + * @param {Router} router + * @param {TranslateService} translate + */ constructor( private notificationsService: NotificationsService, private resourcePolicyService: ResourcePolicyService, @@ -38,6 +47,9 @@ export class ResourcePolicyEditComponent implements OnInit { private translate: TranslateService) { } + /** + * Initialize the component + */ ngOnInit(): void { this.route.data.pipe( map((data) => data), @@ -47,14 +59,27 @@ export class ResourcePolicyEditComponent implements OnInit { }); } + /** + * Return a boolean representing if an operation is pending + * + * @return {Observable} + */ isProcessing(): Observable { return this.processing$.asObservable(); } + /** + * Redirect to the authorizations page + */ redirectToAuthorizationsPage() { this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route }); } + /** + * Update a resource policy + * + * @param event The {{ResourcePolicyEvent}} emitted + */ updateResourcePolicy(event: ResourcePolicyEvent) { this.processing$.next(true); const updatedObject = Object.assign({}, event.object, { From 7df5025aa5961097a7b5867729aac629b0654caf Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 15 Apr 2020 16:23:41 +0200 Subject: [PATCH 047/103] 70373: EPerson impersonate tests --- .../eperson-form.component.spec.ts | 40 +++++++++- .../impersonate-navbar.component.spec.ts | 80 +++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index 7a301aa7e9..16acd14a19 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -42,6 +42,7 @@ describe('EPersonFormComponent', () => { let mockEPeople; let ePersonDataServiceStub: any; + let authService: AuthServiceStub; beforeEach(async(() => { mockEPeople = [EPersonMock, EPersonMock2]; @@ -106,6 +107,7 @@ describe('EPersonFormComponent', () => { }; builderService = getMockFormBuilderService(); translateService = getMockTranslateService(); + authService = new AuthServiceStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ @@ -127,7 +129,7 @@ describe('EPersonFormComponent', () => { { provide: Store, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, - { provide: AuthService, useValue: new AuthServiceStub() }, + { provide: AuthService, useValue: authService }, EPeopleRegistryComponent ], schemas: [NO_ERRORS_SCHEMA] @@ -231,4 +233,40 @@ describe('EPersonFormComponent', () => { }); }); + describe('impersonate', () => { + let ePersonId; + + beforeEach(() => { + spyOn(authService, 'impersonate').and.callThrough(); + ePersonId = 'testEPersonId'; + component.epersonInitial = Object.assign(new EPerson(), { + id: ePersonId + }); + component.impersonate(); + }); + + it('should call authService.impersonate', () => { + expect(authService.impersonate).toHaveBeenCalledWith(ePersonId); + }); + + it('should set isImpersonated to true', () => { + expect(component.isImpersonated).toBe(true); + }); + }); + + describe('stopImpersonating', () => { + beforeEach(() => { + spyOn(authService, 'stopImpersonatingAndRefresh').and.callThrough(); + component.stopImpersonating(); + }); + + it('should call authService.stopImpersonatingAndRefresh', () => { + expect(authService.stopImpersonatingAndRefresh).toHaveBeenCalled(); + }); + + it('should set isImpersonated to false', () => { + expect(component.isImpersonated).toBe(false); + }); + }); + }); diff --git a/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts b/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts index e69de29bb2..16e445f183 100644 --- a/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts +++ b/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts @@ -0,0 +1,80 @@ +import { ImpersonateNavbarComponent } from './impersonate-navbar.component'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { VarDirective } from '../utils/var.directive'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { AuthService } from '../../core/auth/auth.service'; +import { Store, StoreModule } from '@ngrx/store'; +import { authReducer, AuthState } from '../../core/auth/auth.reducer'; +import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; +import { EPersonMock } from '../testing/eperson-mock'; +import { AppState } from '../../app.reducer'; +import { By } from '@angular/platform-browser'; + +describe('ImpersonateNavbarComponent', () => { + let component: ImpersonateNavbarComponent; + let fixture: ComponentFixture; + let authService: AuthService; + let authState: AuthState; + + beforeEach(async(() => { + authService = jasmine.createSpyObj('authService', { + isImpersonating: false, + stopImpersonatingAndRefresh: {} + }); + authState = { + authenticated: true, + loaded: true, + loading: false, + authToken: new AuthTokenInfo('test_token'), + userId: EPersonMock.id + }; + + TestBed.configureTestingModule({ + declarations: [ImpersonateNavbarComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), StoreModule.forRoot(authReducer)], + providers: [ + { provide: AuthService, useValue: authService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(inject([Store], (store: Store) => { + store + .subscribe((state) => { + (state as any).core = Object.create({}); + (state as any).core.auth = authState; + }); + + fixture = TestBed.createComponent(ImpersonateNavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + describe('when the user is impersonating another user', () => { + beforeEach(() => { + component.isImpersonating = true; + fixture.detectChanges(); + }); + + it('should display a button', () => { + const button = fixture.debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + }); + + it('should call authService\'s stopImpersonatingAndRefresh upon clicking the button', () => { + const button = fixture.debugElement.query(By.css('button')).nativeElement; + button.click(); + expect(authService.stopImpersonatingAndRefresh).toHaveBeenCalled(); + }); + }); + + describe('when the user is not impersonating another user', () => { + it('should not display a button', () => { + const button = fixture.debugElement.query(By.css('button')); + expect(button).toBeNull(); + }); + }); +}); From 98ade2eb630dd162c732b39fe6f46705ac817912 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:35:29 +0200 Subject: [PATCH 048/103] Fixed issue with merge --- src/app/core/eperson/group-data.service.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/core/eperson/group-data.service.spec.ts b/src/app/core/eperson/group-data.service.spec.ts index 138cf547f2..3b76e87f0f 100644 --- a/src/app/core/eperson/group-data.service.spec.ts +++ b/src/app/core/eperson/group-data.service.spec.ts @@ -17,7 +17,7 @@ import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson-mock'; import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; -import { SearchParam } from '../cache/models/search-param.model'; +import { RequestParam } from '../cache/models/request-param.model'; import { CoreState } from '../core.reducers'; import { ChangeAnalyzer } from '../data/change-analyzer'; import { PaginatedList } from '../data/paginated-list'; @@ -103,7 +103,7 @@ describe('GroupDataService', () => { it('search with empty query', () => { service.searchGroups(''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', ''))] + searchParams: [Object.assign(new RequestParam('query', ''))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); @@ -111,7 +111,7 @@ describe('GroupDataService', () => { it('search with query', () => { service.searchGroups('test'); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new SearchParam('query', 'test'))] + searchParams: [Object.assign(new RequestParam('query', 'test'))] }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); }); From 5e94cf379c29a9e4d358da5682479c1911ec78c3 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 15 Apr 2020 16:47:18 +0200 Subject: [PATCH 049/103] 70373: AuthService impersonate tests --- src/app/core/auth/auth.service.spec.ts | 117 ++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index 03759987bf..c3b2dcad00 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -8,7 +8,7 @@ import { of as observableOf } from 'rxjs'; import { authReducer, AuthState } from './auth.reducer'; import { NativeWindowRef, NativeWindowService } from '../services/window.service'; -import { AuthService } from './auth.service'; +import { AuthService, IMPERSONATING_COOKIE } from './auth.service'; import { RouterStub } from '../../shared/testing/router-stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { CookieService } from '../services/cookie.service'; @@ -316,5 +316,120 @@ describe('AuthService test', () => { expect(routeServiceMock.getHistory).toHaveBeenCalled(); expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/'); }); + + describe('impersonate', () => { + const userId = 'testUserId'; + + beforeEach(() => { + spyOn(authService, 'refreshAfterLogout'); + authService.impersonate(userId); + }); + + it('should impersonate user', () => { + expect(storage.set).toHaveBeenCalledWith(IMPERSONATING_COOKIE, userId); + }); + + it('should call refreshAfterLogout', () => { + expect(authService.refreshAfterLogout).toHaveBeenCalled(); + }); + }); + + describe('stopImpersonating', () => { + beforeEach(() => { + authService.stopImpersonating(); + }); + + it('should impersonate user', () => { + expect(storage.remove).toHaveBeenCalledWith(IMPERSONATING_COOKIE); + }); + }); + + describe('stopImpersonatingAndRefresh', () => { + beforeEach(() => { + spyOn(authService, 'refreshAfterLogout'); + authService.stopImpersonatingAndRefresh(); + }); + + it('should impersonate user', () => { + expect(storage.remove).toHaveBeenCalledWith(IMPERSONATING_COOKIE); + }); + + it('should call refreshAfterLogout', () => { + expect(authService.refreshAfterLogout).toHaveBeenCalled(); + }); + }); + + describe('getImpersonateID', () => { + beforeEach(() => { + authService.getImpersonateID(); + }); + + it('should impersonate user', () => { + expect(storage.get).toHaveBeenCalledWith(IMPERSONATING_COOKIE); + }); + }); + + describe('isImpersonating', () => { + const userId = 'testUserId'; + let result: boolean; + + describe('when the cookie doesn\'t contain a value', () => { + beforeEach(() => { + result = authService.isImpersonating(); + }); + + it('should return false', () => { + expect(result).toBe(false); + }); + }); + + describe('when the cookie contains a value', () => { + beforeEach(() => { + storage.get = jasmine.createSpy().and.returnValue(userId); + result = authService.isImpersonating(); + }); + + it('should return true', () => { + expect(result).toBe(true); + }); + }); + }); + + describe('isImpersonatingUser', () => { + const userId = 'testUserId'; + let result: boolean; + + describe('when the cookie doesn\'t contain a value', () => { + beforeEach(() => { + result = authService.isImpersonatingUser(userId); + }); + + it('should return false', () => { + expect(result).toBe(false); + }); + }); + + describe('when the cookie contains the right value', () => { + beforeEach(() => { + storage.get = jasmine.createSpy().and.returnValue(userId); + result = authService.isImpersonatingUser(userId); + }); + + it('should return true', () => { + expect(result).toBe(true); + }); + }); + + describe('when the cookie contains the wrong value', () => { + beforeEach(() => { + storage.get = jasmine.createSpy().and.returnValue('wrongValue'); + result = authService.isImpersonatingUser(userId); + }); + + it('should return false', () => { + expect(result).toBe(false); + }); + }); + }); }); }); From a6d7b6444c5ddf385b6f939b2d0f88e065cfa354 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 16 Apr 2020 15:12:41 +0200 Subject: [PATCH 050/103] 70373: Remove unnecessary route navigate --- src/app/core/auth/auth.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index ce0fafe277..d5f76c106b 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -460,7 +460,6 @@ export class AuthService { * Refresh route navigated */ public refreshAfterLogout() { - this.router.navigate(['/home']); // Hard redirect to home page, so that all state is definitely lost this._window.nativeWindow.location.href = '/home'; } From c03146e415dc808903906e9bc0ccd0959dad0bda Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 16 Apr 2020 17:12:20 +0200 Subject: [PATCH 051/103] Fixed resource policy's group edit link --- .../admin-access-control-routing.module.ts | 2 +- src/app/+admin/admin-routing.module.ts | 2 +- src/app/app-routing.module.ts | 2 +- .../resource-policies/resource-policies.component.ts | 7 ++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts b/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts index 5af18c778f..f61a3c2f71 100644 --- a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts +++ b/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts @@ -6,7 +6,7 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { getAccessControlModulePath } from '../admin-routing.module'; -const GROUP_EDIT_PATH = 'groups'; +export const GROUP_EDIT_PATH = 'groups'; export function getGroupEditPath(id: string) { return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString(); diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index aa47c93102..43825aafc8 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -6,7 +6,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { URLCombiner } from '../core/url-combiner/url-combiner'; const REGISTRIES_MODULE_PATH = 'registries'; -const ACCESS_CONTROL_MODULE_PATH = 'access-control'; +export const ACCESS_CONTROL_MODULE_PATH = 'access-control'; export function getRegistriesModulePath() { return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString(); diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 258848ce83..f212709cf3 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -33,7 +33,7 @@ export function getBitstreamModulePath() { return `/${BITSTREAM_MODULE_PATH}`; } -const ADMIN_MODULE_PATH = 'admin'; +export const ADMIN_MODULE_PATH = 'admin'; export function getAdminModulePath() { return `/${ADMIN_MODULE_PATH}`; diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts index 76b23c3001..3fb690111e 100644 --- a/src/app/shared/resource-policies/resource-policies.component.ts +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -22,6 +22,9 @@ import { RequestService } from '../../core/data/request.service'; import { NotificationsService } from '../notifications/notifications.service'; import { dateToString, stringToNgbDateStruct } from '../date.util'; import { followLink } from '../utils/follow-link-config.model'; +import { ADMIN_MODULE_PATH } from '../../app-routing.module'; +import { ACCESS_CONTROL_MODULE_PATH } from '../../+admin/admin-routing.module'; +import { GROUP_EDIT_PATH } from '../../+admin/admin-access-control/admin-access-control-routing.module'; interface ResourcePolicyCheckboxEntry { id: string; @@ -307,7 +310,9 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { filter(() => this.isActive), getFirstSucceededRemoteDataPayload(), map((group: Group) => group.id) - ).subscribe((groupUUID) => this.router.navigate(['groups', groupUUID, 'edit'])) + ).subscribe((groupUUID) => { + this.router.navigate([ADMIN_MODULE_PATH, ACCESS_CONTROL_MODULE_PATH, GROUP_EDIT_PATH, groupUUID]) + }) ) } From 42740809c0419081f23e8e3f958deaa160ac868c Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 21 Apr 2020 16:27:26 +0200 Subject: [PATCH 052/103] changed tasks into wfi's --- ...-search-result-grid-element.component.html | 7 -- ...arch-result-grid-element.component.spec.ts | 87 ------------------- ...ow-search-result-grid-element.component.ts | 47 ---------- ...dmin-workflow-grid-element.component.html} | 2 +- ...dmin-workflow-grid-element.component.scss} | 0 ...n-workflow-grid-element.component.spec.ts} | 12 +-- ...-admin-workflow-grid-element.component.ts} | 40 ++++----- ...-search-result-list-element.component.html | 7 -- ...-search-result-list-element.component.scss | 0 ...arch-result-list-element.component.spec.ts | 68 --------------- ...ow-search-result-list-element.component.ts | 46 ---------- ...admin-workflow-list-element.component.scss | 0 ...m-admin-workflow-list-element.component.ts | 41 --------- ...dmin-workflow-list-element.component.html} | 2 +- ...dmin-workflow-list-element.component.scss} | 0 ...n-workflow-list-element.component.spec.ts} | 10 +-- ...t-admin-workflow-list-element.component.ts | 44 ++++++++++ src/app/+admin/admin.module.ts | 23 ++--- 18 files changed, 82 insertions(+), 354 deletions(-) delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.html delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/{workflow-item-admin-workflow-grid-element.component.html => workflow-item-search-result-admin-workflow-grid-element.component.html} (75%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/{task-search-result/task-admin-workflow-search-result-grid-element.component.scss => workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.scss} (100%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/{workflow-item-admin-workflow-grid-element.component.spec.ts => workflow-item-search-result-admin-workflow-grid-element.component.spec.ts} (81%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/{workflow-item-admin-workflow-grid-element.component.ts => workflow-item-search-result-admin-workflow-grid-element.component.ts} (69%) delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.html delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.scss delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.scss delete mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/{workflow-item-admin-workflow-list-element.component.html => workflow-item-search-result-admin-workflow-list-element.component.html} (80%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/{admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.scss => admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.scss} (100%) rename src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/{workflow-item-admin-workflow-list-element.component.spec.ts => workflow-item-search-result-admin-workflow-list-element.component.spec.ts} (85%) create mode 100644 src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.html deleted file mode 100644 index 8403e632db..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts deleted file mode 100644 index a2461bffda..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { TranslateModule } from '@ngx-translate/core'; -import { Observable } from 'rxjs/internal/Observable'; -import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { Bitstream } from '../../../../../core/shared/bitstream.model'; -import { Item } from '../../../../../core/shared/item.model'; -import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; -import { SharedModule } from '../../../../../shared/shared.module'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; -import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; -import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; -import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { RouterTestingModule } from '@angular/router/testing'; -import { TaskAdminWorkflowSearchResultGridElementComponent } from './task-admin-workflow-search-result-grid-element.component'; -import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; -import { SearchResult } from '../../../../../shared/search/search-result.model'; -import { LinkService } from '../../../../../core/cache/builders/link.service'; -import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; -import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; -import { followLink } from '../../../../../shared/utils/follow-link-config.model'; - -describe('TaskAdminWorkflowSearchResultGridElementComponent', () => { - let component: TaskAdminWorkflowSearchResultGridElementComponent; - let fixture: ComponentFixture; - let id; - let searchResult; - let linkService; - - const mockBitstreamDataService = { - getThumbnailFor(item: Item): Observable> { - return createSuccessfulRemoteDataObject$(new Bitstream()); - } - }; - - function init() { - id = '780b2588-bda5-4112-a1cd-0b15000a5339'; - searchResult = new SearchResult(); - searchResult.indexableObject = new TaskObject(); - searchResult.indexableObject.workflowitem = createSuccessfulRemoteDataObject$(new WorkflowItem()); - searchResult.indexableObject.uuid = id; - linkService = getMockLinkService(); - } - - beforeEach(async(() => { - init(); - TestBed.configureTestingModule( - { - declarations: [TaskAdminWorkflowSearchResultGridElementComponent], - imports: [ - NoopAnimationsModule, - TranslateModule.forRoot(), - RouterTestingModule.withRoutes([]), - SharedModule - ], - providers: [ - { provide: TruncatableService, useValue: mockTruncatableService }, - { provide: BitstreamDataService, useValue: mockBitstreamDataService }, - { provide: LinkService, useValue: linkService }, - ], - schemas: [NO_ERRORS_SCHEMA] - }) - .compileComponents(); - })); - - beforeEach(() => { - linkService.resolveLink.and.callFake((a) => a); - fixture = TestBed.createComponent(TaskAdminWorkflowSearchResultGridElementComponent); - component = fixture.componentInstance; - component.object = searchResult; - component.linkTypes = CollectionElementLinkType; - component.index = 0; - component.viewModes = ViewMode; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should retrieve the workflow item using the link service', () => { - expect(linkService.resolveLink).toHaveBeenCalledWith(searchResult.indexableObject, followLink('workflowitem')); - }); -}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts deleted file mode 100644 index cc002c87c0..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; -import { Context } from '../../../../../core/shared/context.model'; -import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; -import { PoolTaskSearchResult } from '../../../../../shared/object-collection/shared/pool-task-search-result.model'; -import { Observable } from 'rxjs'; -import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; -import { SearchResult } from '../../../../../shared/search/search-result.model'; -import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; -import { ClaimedTaskSearchResult } from '../../../../../shared/object-collection/shared/claimed-task-search-result.model'; -import { LinkService } from '../../../../../core/cache/builders/link.service'; -import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; -import { followLink } from '../../../../../shared/utils/follow-link-config.model'; -import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; - -@listableObjectComponent(PoolTaskSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) -@listableObjectComponent(ClaimedTaskSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) -@Component({ - selector: 'ds-task-admin-workflow-search-result-grid-element', - styleUrls: ['./task-admin-workflow-search-result-grid-element.component.scss'], - templateUrl: './task-admin-workflow-search-result-grid-element.component.html' -}) -/** - * The component for displaying a list element for an task search result on the admin workflow search page - */ -export class TaskAdminWorkflowSearchResultGridElementComponent extends SearchResultGridElementComponent, TaskObject> implements OnInit { - /** - * The workflow item linked to the task object - */ - public wfi$: Observable; - - constructor(private linkService: LinkService, protected truncatableService: TruncatableService, protected bitstreamService: BitstreamDataService) { - super(truncatableService, bitstreamService); - } - - /** - * Initialize the workflow item - */ - ngOnInit(): void { - super.ngOnInit(); - this.dso = this.linkService.resolveLink(this.dso, followLink('workflowitem')); - this.wfi$ = (this.dso.workflowitem as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); - } -} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html similarity index 75% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html index d6b76c0a8d..87bae0c261 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.html +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html @@ -7,6 +7,6 @@
  • - +
diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.scss similarity index 100% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component.scss rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.scss diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts similarity index 81% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts index 4d064444e5..e27a35654b 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts @@ -7,7 +7,7 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { RouterTestingModule } from '@angular/router/testing'; -import { WorkflowItemAdminWorkflowGridElementComponent } from './workflow-item-admin-workflow-grid-element.component'; +import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './workflow-item-search-result-admin-workflow-grid-element.component'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; @@ -18,8 +18,8 @@ import { PublicationGridElementComponent } from '../../../../../shared/object-gr import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; describe('WorkflowItemAdminWorkflowGridElementComponent', () => { - let component: WorkflowItemAdminWorkflowGridElementComponent; - let fixture: ComponentFixture; + let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent; + let fixture: ComponentFixture; let id; let wfi; let itemRD$; @@ -37,7 +37,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { init(); TestBed.configureTestingModule( { - declarations: [WorkflowItemAdminWorkflowGridElementComponent, PublicationGridElementComponent, ListableObjectDirective], + declarations: [WorkflowItemSearchResultAdminWorkflowGridElementComponent, PublicationGridElementComponent, ListableObjectDirective], imports: [ NoopAnimationsModule, TranslateModule.forRoot(), @@ -49,7 +49,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { ], schemas: [NO_ERRORS_SCHEMA] }) - .overrideComponent(WorkflowItemAdminWorkflowGridElementComponent, { + .overrideComponent(WorkflowItemSearchResultAdminWorkflowGridElementComponent, { set: { entryComponents: [PublicationGridElementComponent] } @@ -59,7 +59,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { beforeEach(() => { linkService.resolveLink.and.callFake((a) => a); - fixture = TestBed.createComponent(WorkflowItemAdminWorkflowGridElementComponent); + fixture = TestBed.createComponent(WorkflowItemSearchResultAdminWorkflowGridElementComponent); component = fixture.componentInstance; component.object = wfi; component.linkTypes = CollectionElementLinkType; diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts similarity index 69% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts index 2139590e6e..7abe99cf52 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts @@ -1,44 +1,32 @@ -import { Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { Context } from '../../../../../core/shared/context.model'; -import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; -import { getItemEditPath } from '../../../../../+item-page/item-page-routing.module'; -import { URLCombiner } from '../../../../../core/url-combiner/url-combiner'; -import { - ITEM_EDIT_DELETE_PATH, - ITEM_EDIT_MOVE_PATH, - ITEM_EDIT_PRIVATE_PATH, - ITEM_EDIT_PUBLIC_PATH, - ITEM_EDIT_REINSTATE_PATH, - ITEM_EDIT_WITHDRAW_PATH -} from '../../../../../+item-page/edit-item-page/edit-item-page.routing.module'; import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; -import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; -import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component'; import { Observable } from 'rxjs'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { followLink } from '../../../../../shared/utils/follow-link-config.model'; import { RemoteData } from '../../../../../core/data/remote-data'; import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; import { take } from 'rxjs/operators'; +import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; -@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) +@listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ - selector: 'ds-workflow-item-admin-workflow-grid-element', - styleUrls: ['./workflow-item-admin-workflow-grid-element.component.scss'], - templateUrl: './workflow-item-admin-workflow-grid-element.component.html' + selector: 'ds-workflow-item-search-result-admin-workflow-grid-element', + styleUrls: ['./workflow-item-search-result-admin-workflow-grid-element.component.scss'], + templateUrl: './workflow-item-search-result-admin-workflow-grid-element.component.html' }) /** * The component for displaying a grid element for an workflow item on the admin workflow search page */ -export class WorkflowItemAdminWorkflowGridElementComponent extends AbstractListableElementComponent { +export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent { /** * Directive used to render the dynamic component in */ @@ -59,8 +47,13 @@ export class WorkflowItemAdminWorkflowGridElementComponent extends AbstractLista */ public item$: Observable; - constructor(private componentFactoryResolver: ComponentFactoryResolver, private linkService: LinkService) { - super(); + constructor( + private componentFactoryResolver: ComponentFactoryResolver, + private linkService: LinkService, + protected truncatableService: TruncatableService, + protected bitstreamDataService: BitstreamDataService + ) { + super(truncatableService, bitstreamDataService); } /** @@ -68,8 +61,9 @@ export class WorkflowItemAdminWorkflowGridElementComponent extends AbstractLista * Initialize the item object from the workflow item */ ngOnInit(): void { - this.object = this.linkService.resolveLink(this.object, followLink('item')); - this.item$ = (this.object.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); + super.ngOnInit(); + this.dso = this.linkService.resolveLink(this.dso, followLink('item')); + this.item$ = (this.dso.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); this.item$.pipe(take(1)).subscribe((item: Item) => { const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item)); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.html deleted file mode 100644 index c60347b08a..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts deleted file mode 100644 index f67e653ea4..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; -import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; -import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; -import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { RouterTestingModule } from '@angular/router/testing'; -import { TaskAdminWorkflowSearchResultListElementComponent } from './task-admin-workflow-search-result-list-element.component'; -import { SearchResult } from '../../../../../shared/search/search-result.model'; -import { LinkService } from '../../../../../core/cache/builders/link.service'; -import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; -import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; -import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; -import { followLink } from '../../../../../shared/utils/follow-link-config.model'; - -describe('TaskAdminWorkflowSearchResultListElementComponent', () => { - let component: TaskAdminWorkflowSearchResultListElementComponent; - let fixture: ComponentFixture; - let id; - let searchResult; - let linkService; - - function init() { - id = '780b2588-bda5-4112-a1cd-0b15000a5339'; - searchResult = new SearchResult(); - searchResult.indexableObject = new TaskObject(); - searchResult.indexableObject.workflowitem = createSuccessfulRemoteDataObject$(new WorkflowItem()); - searchResult.indexableObject.uuid = id; - linkService = getMockLinkService(); - } - - beforeEach(async(() => { - init(); - TestBed.configureTestingModule({ - imports: [ - TranslateModule.forRoot(), - RouterTestingModule.withRoutes([]) - ], - declarations: [TaskAdminWorkflowSearchResultListElementComponent], - providers: [{ provide: TruncatableService, useValue: {} }, - { provide: LinkService, useValue: linkService } - ], - schemas: [NO_ERRORS_SCHEMA] - }) - .compileComponents(); - })); - - beforeEach(() => { - linkService.resolveLink.and.callFake((a) => a); - fixture = TestBed.createComponent(TaskAdminWorkflowSearchResultListElementComponent); - component = fixture.componentInstance; - component.object = searchResult; - component.linkTypes = CollectionElementLinkType; - component.index = 0; - component.viewModes = ViewMode; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should retrieve the workflow item using the link service', () => { - expect(linkService.resolveLink).toHaveBeenCalledWith(searchResult.indexableObject, followLink('workflowitem')); - }); -}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts deleted file mode 100644 index 04951f59eb..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component } from '@angular/core'; -import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; -import { Context } from '../../../../../core/shared/context.model'; -import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; -import { PoolTaskSearchResult } from '../../../../../shared/object-collection/shared/pool-task-search-result.model'; -import { Observable, pipe } from 'rxjs'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; -import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; -import { ClaimedTaskSearchResult } from '../../../../../shared/object-collection/shared/claimed-task-search-result.model'; -import { TaskObject } from '../../../../../core/tasks/models/task-object.model'; -import { SearchResult } from '../../../../../shared/search/search-result.model'; -import { followLink } from '../../../../../shared/utils/follow-link-config.model'; -import { LinkService } from '../../../../../core/cache/builders/link.service'; -import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; - -@listableObjectComponent(PoolTaskSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) -@listableObjectComponent(ClaimedTaskSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) -@Component({ - selector: 'ds-task-admin-workflow-search-result-list-element', - styleUrls: ['./task-admin-workflow-search-result-list-element.component.scss'], - templateUrl: './task-admin-workflow-search-result-list-element.component.html' -}) -/** - * The component for displaying a grid element for an task search result on the admin workflow search page - */ -export class TaskAdminWorkflowSearchResultListElementComponent extends SearchResultListElementComponent, TaskObject> { - /** - * The workflow item linked to the task object - */ - public wfi$: Observable; - - constructor(private linkService: LinkService, protected truncatableService: TruncatableService) { - super(truncatableService); - } - - /** - * Initialize the workflow item - */ - ngOnInit(): void { - super.ngOnInit(); - this.dso = this.linkService.resolveLink(this.dso, followLink('workflowitem')); - this.wfi$ = (this.dso.workflowitem as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); - } -} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts deleted file mode 100644 index 835d6acfbb..0000000000 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ViewMode } from '../../../../../core/shared/view-mode.model'; -import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; -import { Context } from '../../../../../core/shared/context.model'; -import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; -import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component'; -import { Observable } from 'rxjs'; -import { LinkService } from '../../../../../core/cache/builders/link.service'; -import { followLink } from '../../../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; -import { Item } from '../../../../../core/shared/item.model'; - -@listableObjectComponent(WorkflowItem, ViewMode.ListElement, Context.AdminWorkflowSearch) -@Component({ - selector: 'ds-workflow-item-admin-workflow-list-element', - styleUrls: ['./workflow-item-admin-workflow-list-element.component.scss'], - templateUrl: './workflow-item-admin-workflow-list-element.component.html' -}) -/** - * The component for displaying a list element for an workflow item on the admin workflow search page - */ -export class WorkflowItemAdminWorkflowListElementComponent extends AbstractListableElementComponent implements OnInit { - - /** - * The item linked to the workflow item - */ - public item$: Observable; - - constructor(private linkService: LinkService) { - super(); - } - - /** - * Initialize the item object from the workflow item - */ - ngOnInit(): void { - this.object = this.linkService.resolveLink(this.object, followLink('item')); - this.item$ = (this.object.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); - } -} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.html similarity index 80% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.html index f3502fe0ab..192cc751f2 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.html +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.html @@ -7,4 +7,4 @@ [index]="index" [linkType]="linkType" [listID]="listID"> - + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.scss similarity index 100% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component.scss rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.scss diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts similarity index 85% rename from src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts rename to src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts index 10a05a8b30..5fb736c1fd 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts @@ -10,7 +10,7 @@ import { CollectionElementLinkType } from '../../../../../shared/object-collecti import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { RouterTestingModule } from '@angular/router/testing'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; -import { WorkflowItemAdminWorkflowListElementComponent } from './workflow-item-admin-workflow-list-element.component'; +import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './workflow-item-search-result-admin-workflow-list-element.component'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; @@ -20,8 +20,8 @@ import { PublicationGridElementComponent } from '../../../../../shared/object-gr import { AdminSidebarSectionComponent } from '../../../../admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; describe('WorkflowItemAdminWorkflowListElementComponent', () => { - let component: WorkflowItemAdminWorkflowListElementComponent; - let fixture: ComponentFixture; + let component: WorkflowItemSearchResultAdminWorkflowListElementComponent; + let fixture: ComponentFixture; let id; let wfi; let itemRD$; @@ -39,7 +39,7 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => { init(); TestBed.configureTestingModule( { - declarations: [WorkflowItemAdminWorkflowListElementComponent], + declarations: [WorkflowItemSearchResultAdminWorkflowListElementComponent], imports: [ NoopAnimationsModule, TranslateModule.forRoot(), @@ -56,7 +56,7 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => { beforeEach(() => { linkService.resolveLink.and.callFake((a) => a); - fixture = TestBed.createComponent(WorkflowItemAdminWorkflowListElementComponent); + fixture = TestBed.createComponent(WorkflowItemSearchResultAdminWorkflowListElementComponent); component = fixture.componentInstance; component.object = wfi; component.linkTypes = CollectionElementLinkType; diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts new file mode 100644 index 0000000000..80225db09f --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit } from '@angular/core'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../../core/shared/context.model'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { Observable } from 'rxjs'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { Item } from '../../../../../core/shared/item.model'; +import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; + +@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) +@Component({ + selector: 'ds-workflow-item-search-result-admin-workflow-list-element', + styleUrls: ['./workflow-item-search-result-admin-workflow-list-element.component.scss'], + templateUrl: './workflow-item-search-result-admin-workflow-list-element.component.html' +}) +/** + * The component for displaying a list element for an workflow item on the admin workflow search page + */ +export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent implements OnInit { + + /** + * The item linked to the workflow item + */ + public item$: Observable; + + constructor(private linkService: LinkService, protected truncatableService: TruncatableService) { + super(truncatableService); + } + + /** + * Initialize the item object from the workflow item + */ + ngOnInit(): void { + super.ngOnInit(); + this.dso = this.linkService.resolveLink(this.dso, followLink('item')); + this.item$ = (this.dso.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); + } +} diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts index cd10aad1ca..25b8bd4648 100644 --- a/src/app/+admin/admin.module.ts +++ b/src/app/+admin/admin.module.ts @@ -12,12 +12,10 @@ import { ItemAdminSearchResultGridElementComponent } from './admin-search-page/a import { CommunityAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component'; import { CollectionAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component'; import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin-search-results/item-admin-search-result-actions.component'; -import { WorkflowItemAdminWorkflowGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-admin-workflow-grid-element.component'; +import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component'; import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component'; -import { WorkflowItemAdminWorkflowListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-admin-workflow-list-element.component'; +import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; -import { TaskAdminWorkflowSearchResultGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/task-search-result/task-admin-workflow-search-result-grid-element.component'; -import { TaskAdminWorkflowSearchResultListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/task-item-search-result/task-admin-workflow-search-result-list-element.component'; @NgModule({ imports: [ @@ -38,12 +36,10 @@ import { TaskAdminWorkflowSearchResultListElementComponent } from './admin-workf CollectionAdminSearchResultGridElementComponent, ItemAdminSearchResultActionsComponent, - WorkflowItemAdminWorkflowListElementComponent, - WorkflowItemAdminWorkflowGridElementComponent, - WorkflowItemAdminWorkflowActionsComponent, + WorkflowItemSearchResultAdminWorkflowListElementComponent, + WorkflowItemSearchResultAdminWorkflowGridElementComponent, + WorkflowItemAdminWorkflowActionsComponent - TaskAdminWorkflowSearchResultGridElementComponent, - TaskAdminWorkflowSearchResultListElementComponent, ], entryComponents: [ ItemAdminSearchResultListElementComponent, @@ -54,12 +50,9 @@ import { TaskAdminWorkflowSearchResultListElementComponent } from './admin-workf CollectionAdminSearchResultGridElementComponent, ItemAdminSearchResultActionsComponent, - WorkflowItemAdminWorkflowListElementComponent, - WorkflowItemAdminWorkflowGridElementComponent, - WorkflowItemAdminWorkflowActionsComponent, - - TaskAdminWorkflowSearchResultGridElementComponent, - TaskAdminWorkflowSearchResultListElementComponent, + WorkflowItemSearchResultAdminWorkflowListElementComponent, + WorkflowItemSearchResultAdminWorkflowGridElementComponent, + WorkflowItemAdminWorkflowActionsComponent ] }) export class AdminModule { From 6196a99a989c148a260ccd2e957c9f5aeaeea7c8 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 21 Apr 2020 18:41:38 +0200 Subject: [PATCH 053/103] fixed tests --- ...-result-admin-workflow-grid-element.component.spec.ts | 8 +++++++- ...-result-admin-workflow-list-element.component.spec.ts | 9 +++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts index e27a35654b..0fe52373ec 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts @@ -16,6 +16,8 @@ import { followLink } from '../../../../../shared/utils/follow-link-config.model import { Item } from '../../../../../core/shared/item.model'; import { PublicationGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component'; import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; +import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; describe('WorkflowItemAdminWorkflowGridElementComponent', () => { let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent; @@ -24,12 +26,15 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { let wfi; let itemRD$; let linkService; + let object; function init() { itemRD$ = createSuccessfulRemoteDataObject$(new Item()); id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + object = new WorkflowItemSearchResult() wfi = new WorkflowItem(); wfi.item = itemRD$; + object.indexableObject = wfi; linkService = getMockLinkService(); } @@ -46,6 +51,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { providers: [ { provide: LinkService, useValue: linkService }, { provide: TruncatableService, useValue: {} }, + { provide: BitstreamDataService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA] }) @@ -61,7 +67,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => { linkService.resolveLink.and.callFake((a) => a); fixture = TestBed.createComponent(WorkflowItemSearchResultAdminWorkflowGridElementComponent); component = fixture.componentInstance; - component.object = wfi; + component.object = object; component.linkTypes = CollectionElementLinkType; component.index = 0; component.viewModes = ViewMode; diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts index 5fb736c1fd..c38915940c 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts @@ -4,7 +4,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; -import { SharedModule } from '../../../../../shared/shared.module'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; @@ -16,8 +15,7 @@ import { getMockLinkService } from '../../../../../shared/mocks/mock-link-servic import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; import { followLink } from '../../../../../shared/utils/follow-link-config.model'; import { Item } from '../../../../../core/shared/item.model'; -import { PublicationGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component'; -import { AdminSidebarSectionComponent } from '../../../../admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; +import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; describe('WorkflowItemAdminWorkflowListElementComponent', () => { let component: WorkflowItemSearchResultAdminWorkflowListElementComponent; @@ -26,12 +24,15 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => { let wfi; let itemRD$; let linkService; + let object; function init() { itemRD$ = createSuccessfulRemoteDataObject$(new Item()); id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + object = new WorkflowItemSearchResult() wfi = new WorkflowItem(); wfi.item = itemRD$; + object.indexableObject = wfi; linkService = getMockLinkService(); } @@ -58,7 +59,7 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => { linkService.resolveLink.and.callFake((a) => a); fixture = TestBed.createComponent(WorkflowItemSearchResultAdminWorkflowListElementComponent); component = fixture.componentInstance; - component.object = wfi; + component.object = object; component.linkTypes = CollectionElementLinkType; component.index = 0; component.viewModes = ViewMode; From c2789dfbf7f7602a4457f03909e02b25a8f1ffda Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 24 Apr 2020 13:16:52 +0200 Subject: [PATCH 054/103] fix issue where a hard refresh wouldn't work to clear the state due to the browser caching the html --- src/app/app-routing.module.ts | 1 + src/app/app.metareducers.ts | 19 ++++++++++++++++++- src/app/core/auth/auth.service.ts | 5 +++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 258848ce83..168c828c79 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -60,6 +60,7 @@ export function getDSOPath(dso: DSpaceObject): string { imports: [ RouterModule.forRoot([ { path: '', redirectTo: '/home', pathMatch: 'full' }, + { path: 'reload/:rnd', redirectTo: '/home', pathMatch: 'full' }, { path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } }, { path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' }, { path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, diff --git a/src/app/app.metareducers.ts b/src/app/app.metareducers.ts index 131d240b79..7675840969 100644 --- a/src/app/app.metareducers.ts +++ b/src/app/app.metareducers.ts @@ -1,4 +1,5 @@ import { StoreActionTypes } from './store.actions'; +import { AuthActionTypes } from './core/auth/auth.actions'; // fallback ngrx debugger let actionCounter = 0; @@ -28,10 +29,26 @@ export function universalMetaReducer(reducer) { } } +// const clearStateActions = [ +// AuthActionTypes.LOG_OUT_SUCCESS, +// ]; +// +// export function clearStateMetaReducer(reducer) { +// return function (state, action) { +// +// if (clearStateActions.includes(action.type)) { +// state = {}; +// } +// +// return reducer(state, action); +// }; +// } + export const debugMetaReducers = [ debugMetaReducer ]; export const appMetaReducers = [ - universalMetaReducer + universalMetaReducer, + // clearStateMetaReducer ]; diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index d5f76c106b..588d9e2675 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -460,8 +460,9 @@ export class AuthService { * Refresh route navigated */ public refreshAfterLogout() { - // Hard redirect to home page, so that all state is definitely lost - this._window.nativeWindow.location.href = '/home'; + // Hard redirect to the reload page with a unique number behind it + // so that all state is definitely lost + this._window.nativeWindow.location.href = `/reload/${new Date().getTime()}`; } /** From df2cc2217250e29468b8b85c0178352ae1235e1b Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 24 Apr 2020 14:06:37 +0200 Subject: [PATCH 055/103] remove clearStateMetaReducer --- src/app/app.metareducers.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/app/app.metareducers.ts b/src/app/app.metareducers.ts index 7675840969..131d240b79 100644 --- a/src/app/app.metareducers.ts +++ b/src/app/app.metareducers.ts @@ -1,5 +1,4 @@ import { StoreActionTypes } from './store.actions'; -import { AuthActionTypes } from './core/auth/auth.actions'; // fallback ngrx debugger let actionCounter = 0; @@ -29,26 +28,10 @@ export function universalMetaReducer(reducer) { } } -// const clearStateActions = [ -// AuthActionTypes.LOG_OUT_SUCCESS, -// ]; -// -// export function clearStateMetaReducer(reducer) { -// return function (state, action) { -// -// if (clearStateActions.includes(action.type)) { -// state = {}; -// } -// -// return reducer(state, action); -// }; -// } - export const debugMetaReducers = [ debugMetaReducer ]; export const appMetaReducers = [ - universalMetaReducer, - // clearStateMetaReducer + universalMetaReducer ]; From b338332d13ca1ecef18ced09d35b821796b468e6 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Apr 2020 15:43:52 +0200 Subject: [PATCH 056/103] Added possibility to search for eperson or group while adding a new resource policy --- resources/i18n/en.json5 | 2 +- .../eperson-group-list.component.html | 5 +- .../eperson-group-list.component.spec.ts | 48 +++++--- .../eperson-group-list.component.ts | 59 +++++++-- .../eperson-search-box.component.html | 26 ++++ .../eperson-search-box.component.spec.ts | 115 ++++++++++++++++++ .../eperson-search-box.component.ts | 65 ++++++++++ .../group-search-box.component.html | 20 +++ .../group-search-box.component.spec.ts | 114 +++++++++++++++++ .../group-search-box.component.ts | 61 ++++++++++ src/app/shared/shared.module.ts | 6 +- 11 files changed, 488 insertions(+), 33 deletions(-) create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.html create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.ts create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.html create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.spec.ts create mode 100644 src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index ada2a1d404..96b4729caa 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -2021,7 +2021,7 @@ "resource-policies.form.action-type.required": "You must select the resource policy action.", - "resource-policies.form.eperson-group-list.label": "Select the eperson or group that will be grant of the permission", + "resource-policies.form.eperson-group-list.label": "The eperson or group that will be grant of the permission", "resource-policies.form.eperson-group-list.select.btn": "Select", diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.html b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.html index 729236da93..ce6eccb723 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.html +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.html @@ -1,4 +1,7 @@ -
+
+ + + { let comp: EpersonGroupListComponent; let compAsAny: any; let fixture: ComponentFixture; let de; - let scheduler: TestScheduler; + let groupService: any; + let epersonService: any; const paginationOptions: PaginationComponentOptions = new PaginationComponentOptions() paginationOptions.id = uniqueId('eperson-group-list-pagination-test'); paginationOptions.pageSize = 5; - const epersonService = jasmine.createSpyObj('epersonService', + const mockEpersonService = jasmine.createSpyObj('epersonService', { findByHref: jasmine.createSpy('findByHref'), findAll: jasmine.createSpy('findAll'), + searchByScope: jasmine.createSpy('searchByScope'), }, { linkPath: 'epersons' } ); - const groupService = jasmine.createSpyObj('groupService', + const mockGroupService = jasmine.createSpyObj('groupService', { findByHref: jasmine.createSpy('findByHref'), findAll: jasmine.createSpy('findAll'), + searchGroups: jasmine.createSpy('searchGroups'), }, { linkPath: 'groups' @@ -59,6 +62,7 @@ describe('EpersonGroupListComponent test suite', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ + NoopAnimationsModule, TranslateModule.forRoot() ], declarations: [ @@ -66,8 +70,8 @@ describe('EpersonGroupListComponent test suite', () => { TestComponent ], providers: [ - { provide: EPersonDataService, useValue: epersonService }, - { provide: GroupDataService, useValue: groupService }, + { provide: EPersonDataService, useValue: mockEpersonService }, + { provide: GroupDataService, useValue: mockGroupService }, { provide: RequestService, useValue: getMockRequestService() }, EpersonGroupListComponent, ChangeDetectorRef, @@ -108,6 +112,7 @@ describe('EpersonGroupListComponent test suite', () => { beforeEach(() => { // initTestScheduler(); fixture = TestBed.createComponent(EpersonGroupListComponent); + epersonService = TestBed.get(EPersonDataService); comp = fixture.componentInstance; compAsAny = fixture.componentInstance; comp.isListOfEPerson = true; @@ -138,11 +143,8 @@ describe('EpersonGroupListComponent test suite', () => { }); it('should init the list of eperson', () => { - compAsAny.dataService.findAll.and.returnValue(observableOf(epersonPaginatedListRD)); - - scheduler = getTestScheduler(); - scheduler.schedule(() => comp.updateList(paginationOptions)); - scheduler.flush(); + epersonService.searchByScope.and.returnValue(observableOf(epersonPaginatedListRD)); + fixture.detectChanges(); expect(compAsAny.list$.value).toEqual(epersonPaginatedListRD); expect(comp.getList()).toBeObservable(cold('a', { @@ -187,6 +189,7 @@ describe('EpersonGroupListComponent test suite', () => { beforeEach(() => { // initTestScheduler(); fixture = TestBed.createComponent(EpersonGroupListComponent); + groupService = TestBed.get(GroupDataService); comp = fixture.componentInstance; compAsAny = fixture.componentInstance; comp.isListOfEPerson = false; @@ -217,11 +220,8 @@ describe('EpersonGroupListComponent test suite', () => { }); it('should init the list of group', () => { - compAsAny.dataService.findAll.and.returnValue(observableOf(groupPaginatedListRD)); - - scheduler = getTestScheduler(); - scheduler.schedule(() => comp.updateList(paginationOptions)); - scheduler.flush(); + groupService.searchGroups.and.returnValue(observableOf(groupPaginatedListRD)); + fixture.detectChanges(); expect(compAsAny.list$.value).toEqual(groupPaginatedListRD); expect(comp.getList()).toBeObservable(cold('a', { @@ -259,6 +259,18 @@ describe('EpersonGroupListComponent test suite', () => { expect(compAsAny.updateList).toHaveBeenCalled(); }); + + it('should update list on search triggered', () => { + const options: PaginationComponentOptions = comp.paginationOptions + const event: SearchEvent = { + scope: 'metadata', + query: 'test' + } + spyOn(comp, 'updateList'); + comp.onSearch(event); + + expect(compAsAny.updateList).toHaveBeenCalledWith(options, 'metadata', 'test'); + }); }); }); diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts index fd033d8728..02c4726d45 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts @@ -16,11 +16,22 @@ import { getDataServiceFor } from '../../../../core/cache/builders/build-decorat import { EPERSON } from '../../../../core/eperson/models/eperson.resource-type'; import { GROUP } from '../../../../core/eperson/models/group.resource-type'; import { ResourceType } from '../../../../core/shared/resource-type'; +import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { fadeInOut } from '../../../animations/fade'; + +export interface SearchEvent { + scope: string; + query: string +} @Component({ selector: 'ds-eperson-group-list', styleUrls: ['./eperson-group-list.component.scss'], - templateUrl: './eperson-group-list.component.html' + templateUrl: './eperson-group-list.component.html', + animations: [ + fadeInOut + ] }) /** * Component that shows a list of eperson or group @@ -43,6 +54,16 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { */ @Output() select: EventEmitter = new EventEmitter(); + /** + * Current search query + */ + public currentSearchQuery = ''; + + /** + * Current search scope + */ + public currentSearchScope = 'metadata'; + /** * Pagination config used to display the list */ @@ -52,7 +73,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { * The data service used to make request. * It could be EPersonDataService or GroupDataService */ - private readonly dataService: DataService; + private dataService: DataService; /** * A list of eperson or group @@ -78,18 +99,18 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { * @param {Injector} parentInjector */ constructor(public dsoNameService: DSONameService, private parentInjector: Injector) { - const resourceType: ResourceType = (this.isListOfEPerson) ? EPERSON : GROUP; - const provider = getDataServiceFor(resourceType); - this.dataService = Injector.create({ - providers: [], - parent: this.parentInjector - }).get(provider); } /** * Initialize the component */ ngOnInit(): void { + const resourceType: ResourceType = (this.isListOfEPerson) ? EPERSON : GROUP; + const provider = getDataServiceFor(resourceType); + this.dataService = Injector.create({ + providers: [], + parent: this.parentInjector + }).get(provider); this.paginationOptions.id = uniqueId('eperson-group-list-pagination'); this.paginationOptions.pageSize = 5; @@ -97,7 +118,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { this.entrySelectedId.next(this.initSelected); } - this.updateList(this.paginationOptions); + this.updateList(this.paginationOptions, this.currentSearchScope, this.currentSearchQuery); } /** @@ -134,19 +155,33 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { */ onPageChange(page: number): void { this.paginationOptions.currentPage = page; - this.updateList(this.paginationOptions); + this.updateList(this.paginationOptions, this.currentSearchScope, this.currentSearchQuery); + } + + /** + * Method called on search + */ + onSearch(searchEvent: SearchEvent) { + this.currentSearchQuery = searchEvent.query; + this.currentSearchScope = searchEvent.scope; + this.paginationOptions.currentPage = 1; + this.updateList(this.paginationOptions, this.currentSearchScope, this.currentSearchQuery); } /** * Retrieve a paginate list of eperson or group */ - updateList(config: PaginationComponentOptions): void { + updateList(config: PaginationComponentOptions, scope: string, query: string): void { const options: FindListOptions = Object.assign({}, new FindListOptions(), { elementsPerPage: config.pageSize, currentPage: config.currentPage }); - this.subs.push(this.dataService.findAll(options).pipe(take(1)) + const search$: Observable>> = this.isListOfEPerson ? + (this.dataService as EPersonDataService).searchByScope(scope, query, options) : + (this.dataService as GroupDataService).searchGroups(query, options); + + this.subs.push(search$.pipe(take(1)) .subscribe((list: RemoteData>) => { this.list$.next(list) }) diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.html b/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.html new file mode 100644 index 0000000000..0d130c723c --- /dev/null +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.html @@ -0,0 +1,26 @@ +
+
+ +
+
+
+ + + + +
+
+
+ +
+
diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts new file mode 100644 index 0000000000..5b0456e6a4 --- /dev/null +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts @@ -0,0 +1,115 @@ +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TranslateModule } from '@ngx-translate/core'; + +import { createTestComponent } from '../../../../testing/utils'; +import { EpersonSearchBoxComponent } from './eperson-search-box.component'; +import { SearchEvent } from '../eperson-group-list.component'; + +describe('EpersonSearchBoxComponent test suite', () => { + let comp: EpersonSearchBoxComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let de; + let formBuilder: FormBuilder; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + FormsModule, + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + EpersonSearchBoxComponent, + TestComponent + ], + providers: [ + FormBuilder, + EpersonSearchBoxComponent + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create EpersonSearchBoxComponent', inject([EpersonSearchBoxComponent], (app: EpersonSearchBoxComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('', () => { + beforeEach(() => { + // initTestScheduler(); + fixture = TestBed.createComponent(EpersonSearchBoxComponent); + formBuilder = TestBed.get(FormBuilder); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should reset the form', () => { + comp.searchForm = formBuilder.group(({ + query: 'test', + })); + + comp.reset(); + + expect(comp.searchForm.controls.query.value).toBe(''); + }); + + it('should emit new search event', () => { + const data = { + scope: 'metadata', + query: 'test' + } + + const event: SearchEvent = { + scope: 'metadata', + query: 'test' + } + spyOn(comp.search, 'emit'); + + comp.submit(data); + + expect(comp.search.emit).toHaveBeenCalledWith(event); + }); + }) +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.ts new file mode 100644 index 0000000000..04c5cafd3f --- /dev/null +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.ts @@ -0,0 +1,65 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; + +import { Subscription } from 'rxjs'; + +import { SearchEvent } from '../eperson-group-list.component'; +import { isNotNull } from '../../../../empty.util'; + +/** + * A component used to show a search box for epersons. + */ +@Component({ + selector: 'ds-eperson-search-box', + templateUrl: './eperson-search-box.component.html', +}) +export class EpersonSearchBoxComponent { + + labelPrefix = 'admin.access-control.epeople.'; + + /** + * The search form + */ + searchForm; + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + + /** + * An event fired when a search is triggred. + * Event's payload is a SearchEvent. + */ + @Output() search: EventEmitter = new EventEmitter(); + + constructor(private formBuilder: FormBuilder) { + this.searchForm = this.formBuilder.group(({ + scope: 'metadata', + query: '', + })); + } + + /** + * Reset the search form + */ + reset() { + this.searchForm = this.formBuilder.group(({ + scope: 'metadata', + query: '', + })); + } + + /** + * Emit a new search event + * @param data Form data + */ + submit(data: any) { + const event: SearchEvent = { + scope: isNotNull(data) ? data.scope : 'metadata', + query: isNotNull(data) ? data.query : '' + } + + this.search.emit(event) + } +} diff --git a/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.html b/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.html new file mode 100644 index 0000000000..418996c564 --- /dev/null +++ b/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.html @@ -0,0 +1,20 @@ +
+
+
+ + + + +
+
+
+ +
+
diff --git a/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.spec.ts b/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.spec.ts new file mode 100644 index 0000000000..b23e69c37c --- /dev/null +++ b/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.spec.ts @@ -0,0 +1,114 @@ +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { TranslateModule } from '@ngx-translate/core'; + +import { createTestComponent } from '../../../../testing/utils'; +import { GroupSearchBoxComponent } from './group-search-box.component'; +import { SearchEvent } from '../eperson-group-list.component'; + +describe('GroupSearchBoxComponent test suite', () => { + let comp: GroupSearchBoxComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let de; + let formBuilder: FormBuilder; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + FormsModule, + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + GroupSearchBoxComponent, + TestComponent + ], + providers: [ + FormBuilder, + GroupSearchBoxComponent + ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create GroupSearchBoxComponent', inject([GroupSearchBoxComponent], (app: GroupSearchBoxComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('', () => { + beforeEach(() => { + // initTestScheduler(); + fixture = TestBed.createComponent(GroupSearchBoxComponent); + formBuilder = TestBed.get(FormBuilder); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should reset the form', () => { + comp.searchForm = formBuilder.group(({ + query: 'test', + })); + + comp.reset(); + + expect(comp.searchForm.controls.query.value).toBe(''); + }); + + it('should emit new search event', () => { + const data = { + query: 'test' + } + + const event: SearchEvent = { + scope: '', + query: 'test' + } + spyOn(comp.search, 'emit'); + + comp.submit(data); + + expect(comp.search.emit).toHaveBeenCalledWith(event); + }); + }) +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.ts new file mode 100644 index 0000000000..5f4feb582f --- /dev/null +++ b/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.ts @@ -0,0 +1,61 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; + +import { Subscription } from 'rxjs'; + +import { SearchEvent } from '../eperson-group-list.component'; + +/** + * A component used to show a search box for groups. + */ +@Component({ + selector: 'ds-group-search-box', + templateUrl: './group-search-box.component.html', +}) +export class GroupSearchBoxComponent { + + labelPrefix = 'admin.access-control.groups.'; + + /** + * The search form + */ + searchForm; + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + + /** + * An event fired when a search is triggred. + * Event's payload is a SearchEvent. + */ + @Output() search: EventEmitter = new EventEmitter(); + + constructor(private formBuilder: FormBuilder) { + this.searchForm = this.formBuilder.group(({ + query: '', + })); + } + + /** + * Reset the search form + */ + reset() { + this.searchForm = this.formBuilder.group(({ + query: '', + })); + } + + /** + * Emit a new search event + * @param data Form data + */ + submit(data: any) { + const event: SearchEvent = { + scope: '', + query: data.query + } + this.search.emit(event) + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 6c59ba9d93..60c2d42717 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -198,6 +198,8 @@ import { ResourcePolicyFormComponent } from './resource-policies/form/resource-p import { EpersonGroupListComponent } from './resource-policies/form/eperson-group-list/eperson-group-list.component'; import { ResourcePolicyTargetResolver } from './resource-policies/resolvers/resource-policy-target.resolver'; import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver'; +import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component'; +import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -378,7 +380,9 @@ const COMPONENTS = [ ItemVersionsNoticeComponent, ResourcePoliciesComponent, ResourcePolicyFormComponent, - EpersonGroupListComponent + EpersonGroupListComponent, + EpersonSearchBoxComponent, + GroupSearchBoxComponent ]; const ENTRY_COMPONENTS = [ From bacb778fa7b0f85bfaeaf89ece1262f1a43ed6b0 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Apr 2020 17:25:00 +0200 Subject: [PATCH 057/103] Fixed resource policy page buttons --- resources/i18n/en.json5 | 6 +++++- .../resource-policies.component.html | 21 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 96b4729caa..3868be5d41 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1987,6 +1987,8 @@ + "resource-policies.add.button": "Add", + "resource-policies.add.for.": "Add a new policy", "resource-policies.add.for.bitstream": "Add a new Bitstream policy", @@ -2003,7 +2005,9 @@ "resource-policies.create.page.title": "Create new resource policy", - "resource-policies.delete.btn": "Delete selected resource policies", + "resource-policies.delete.btn": "Delete selected", + + "resource-policies.delete.btn.title": "Delete selected resource policies", "resource-policies.delete.failure.content": "An error occurred while deleting selected resource policies.", diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index 8209c836ff..2a2c013ecb 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -2,24 +2,28 @@ - + @@ -59,9 +64,6 @@ @@ -79,6 +81,11 @@ +
+
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} {{resourceUUID}}
- -
@@ -43,6 +47,7 @@
{{'resource-policies.table.headers.group' | translate}} {{'resource-policies.table.headers.date.start' | translate}} {{'resource-policies.table.headers.date.end' | translate}}{{'resource-policies.table.headers.edit' | translate}}
{{entry.id}} - {{entry.policy.name}} {{entry.policy.policyType}} {{formatDate(entry.policy.startDate)}} {{formatDate(entry.policy.endDate)}} + +
From 6220c51aa612b19325900b5ad73d37e7cbb9a8d8 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 11 May 2020 13:28:02 +0200 Subject: [PATCH 058/103] fixed tests --- ...t-admin-workflow-grid-element.component.spec.ts | 4 ++-- ...t-admin-workflow-list-element.component.spec.ts | 4 ++-- .../workflow-item-action-page.component.spec.ts | 12 ++++++------ .../workflow-item-delete.component.spec.ts | 14 +++++++------- .../workflow-item-send-back.component.spec.ts | 14 +++++++------- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts index 0fe52373ec..2f3f88fa70 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts @@ -10,14 +10,14 @@ import { RouterTestingModule } from '@angular/router/testing'; import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './workflow-item-search-result-admin-workflow-grid-element.component'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { LinkService } from '../../../../../core/cache/builders/link.service'; -import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; import { followLink } from '../../../../../shared/utils/follow-link-config.model'; import { Item } from '../../../../../core/shared/item.model'; import { PublicationGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component'; import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; +import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; describe('WorkflowItemAdminWorkflowGridElementComponent', () => { let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent; diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts index c38915940c..53f81f96db 100644 --- a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts @@ -11,11 +11,11 @@ import { RouterTestingModule } from '@angular/router/testing'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './workflow-item-search-result-admin-workflow-list-element.component'; import { LinkService } from '../../../../../core/cache/builders/link.service'; -import { getMockLinkService } from '../../../../../shared/mocks/mock-link-service'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; import { followLink } from '../../../../../shared/utils/follow-link-config.model'; import { Item } from '../../../../../core/shared/item.model'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; +import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; describe('WorkflowItemAdminWorkflowListElementComponent', () => { let component: WorkflowItemSearchResultAdminWorkflowListElementComponent; diff --git a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts index 71d9346b29..979476bf03 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts @@ -2,13 +2,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { WorkflowItemActionPageComponent } from './workflow-item-action-page.component'; -import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/testing/utils'; -import { ActivatedRouteStub } from '../shared/testing/active-router-stub'; -import { NotificationsServiceStub } from '../shared/testing/notifications-service-stub'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { RouteService } from '../core/services/route.service'; -import { RouterStub } from '../shared/testing/router-stub'; import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -16,6 +11,11 @@ import { WorkflowItem } from '../core/submission/models/workflowitem.model'; import { Observable, of as observableOf } from 'rxjs'; import { VarDirective } from '../shared/utils/var.directive'; import { By } from '@angular/platform-browser'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; +import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; +import { RouterStub } from '../shared/testing/router.stub'; +import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub'; const type = 'testType'; describe('WorkflowItemActionPageComponent', () => { @@ -42,7 +42,7 @@ describe('WorkflowItemActionPageComponent', () => { imports: [TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } })], declarations: [TestComponent, VarDirective], diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts index c11cdc1d15..a70005776b 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts @@ -2,21 +2,21 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { ActivatedRoute, Router } from '@angular/router'; -import { RouterStub } from '../../shared/testing/router-stub'; -import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { RouteService } from '../../core/services/route.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { VarDirective } from '../../shared/utils/var.directive'; import { of as observableOf } from 'rxjs'; import { RequestService } from '../../core/data/request.service'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; describe('WorkflowItemDeleteComponent', () => { let component: WorkflowItemDeleteComponent; @@ -42,7 +42,7 @@ describe('WorkflowItemDeleteComponent', () => { imports: [TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } })], declarations: [WorkflowItemDeleteComponent, VarDirective], diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts index 0332a6b9ee..fde48b59e4 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts @@ -1,22 +1,22 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { ActivatedRoute, Router } from '@angular/router'; -import { RouterStub } from '../../shared/testing/router-stub'; -import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { RouteService } from '../../core/services/route.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { VarDirective } from '../../shared/utils/var.directive'; import { of as observableOf } from 'rxjs'; import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; import { RequestService } from '../../core/data/request.service'; -import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; describe('WorkflowItemSendBackComponent', () => { let component: WorkflowItemSendBackComponent; @@ -42,7 +42,7 @@ describe('WorkflowItemSendBackComponent', () => { imports: [TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: MockTranslateLoader + useClass: TranslateLoaderMock } })], declarations: [WorkflowItemSendBackComponent, VarDirective], From 03668e347e1c425d7113daf0803eacea0ac98cfb Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 12 May 2020 17:03:48 +0200 Subject: [PATCH 059/103] added missing typedoc --- .../submission/workflowitem-data.service.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index 370b2df6dd..c82f7bf0b5 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -19,7 +19,7 @@ import { hasValue } from '../../shared/empty.util'; import { RequestEntry } from '../data/request.reducer'; /** - * A service that provides methods to make REST requests with workflowitems endpoint. + * A service that provides methods to make REST requests with workflow items endpoint. */ @Injectable() @dataService(WorkflowItem.type) @@ -40,18 +40,30 @@ export class WorkflowItemDataService extends DataService { } /** - * Delete an existing Workspace Item on the server - * @param id The Workspace Item's id to be removed + * Delete an existing Workflow Item on the server + * @param id The Workflow Item's id to be removed * @return an observable that emits true when the deletion was successful, false when it failed */ delete(id: string): Observable { return this.deleteWFI(id, true) } + /** + * Send an existing Workflow Item back to the workflow on the server + * @param id The Workspace Item's id to be sent back + * @return an observable that emits true when sending back the item was successful, false when it failed + */ sendBack(id: string): Observable { return this.deleteWFI(id, false) } + /** + * Method to delete a workflow item from the server + * @param id The identifier of the server + * @param expunge Whether or not to expunge: + * When true, the workflow item and its item will be permanently expunged on the server + * When false, the workflow item will be removed, but the item will still be available as a workspace item + */ private deleteWFI(id: string, expunge: boolean): Observable { const requestId = this.requestService.generateRequestId(); From 3894b3615d4d788c9d760c965ca87c367655d0d8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 12 May 2020 17:20:02 +0200 Subject: [PATCH 060/103] 70373: Test import fix --- .../impersonate-navbar/impersonate-navbar.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts b/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts index 16e445f183..e6ddc3075d 100644 --- a/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts +++ b/src/app/shared/impersonate-navbar/impersonate-navbar.component.spec.ts @@ -8,7 +8,7 @@ import { AuthService } from '../../core/auth/auth.service'; import { Store, StoreModule } from '@ngrx/store'; import { authReducer, AuthState } from '../../core/auth/auth.reducer'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; -import { EPersonMock } from '../testing/eperson-mock'; +import { EPersonMock } from '../testing/eperson.mock'; import { AppState } from '../../app.reducer'; import { By } from '@angular/platform-browser'; From 03f910b45dde663920bfa029599c91d1398a3edb Mon Sep 17 00:00:00 2001 From: Bram Luyten Date: Mon, 18 May 2020 15:52:16 +0200 Subject: [PATCH 061/103] Ensuring environment.ts is present before sync-i18n-files invocation --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6fd39420b5..4c6bd31cac 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld", "clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:env", "clean:env": "rimraf src/environments/environment.ts", - "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts" + "sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts" }, "browser": { "fs": false, From b70f8e12f6478d524939300afc176c6d5ddbdc86 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 20 May 2020 17:05:54 +0200 Subject: [PATCH 062/103] Fixed merge with master --- .../item-authorizations.component.spec.ts | 22 ++++--------------- .../resource-policy.service.spec.ts | 3 +-- .../resource-policy-create.component.spec.ts | 14 ++++++------ .../resource-policy-edit.component.spec.ts | 12 +++++----- .../eperson-group-list.component.spec.ts | 7 +++--- .../eperson-search-box.component.spec.ts | 2 +- .../group-search-box.component.spec.ts | 2 +- .../resource-policy-form.component.spec.ts | 9 ++++---- .../resource-policies.component.spec.ts | 14 ++++++------ 9 files changed, 36 insertions(+), 49 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts index 5447b09167..c687c829eb 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts @@ -13,10 +13,9 @@ import { Bundle } from '../../../core/shared/bundle.model'; import { createMockRDPaginatedObs } from '../item-bitstreams/item-bitstreams.component.spec'; import { Item } from '../../../core/shared/item.model'; import { LinkService } from '../../../core/cache/builders/link.service'; -import { getMockLinkService } from '../../../shared/mocks/mock-link-service'; -import { createSuccessfulRemoteDataObject, createTestComponent } from '../../../shared/testing/utils'; -import { getMockResourcePolicyService } from '../../../shared/mocks/mock-resource-policy-service'; -import { RouterStub } from '../../../shared/testing/router-stub'; +import { getMockLinkService } from '../../../shared/mocks/link-service.mock'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { createTestComponent } from '../../../shared/testing/utils.test'; import { PaginatedList } from '../../../core/data/paginated-list'; import { PageInfo } from '../../../core/shared/page-info.model'; @@ -25,8 +24,7 @@ describe('ItemAuthorizationsComponent test suite', () => { let compAsAny: any; let fixture: ComponentFixture; let de; - let routerStub: any; - const resourcePolicyService: any = getMockResourcePolicyService(); + const linkService: any = getMockLinkService(); const bitstream1 = Object.assign(new Bitstream(), { @@ -80,18 +78,6 @@ describe('ItemAuthorizationsComponent test suite', () => { }) }; - const epersonService = jasmine.createSpyObj('epersonService', { - findByHref: jasmine.createSpy('findByHref'), - }); - - const groupService = jasmine.createSpyObj('groupService', { - findByHref: jasmine.createSpy('findByHref'), - }); - - routerStub = Object.assign(new RouterStub(), { - url: `url/edit` - }); - beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ diff --git a/src/app/core/resource-policy/resource-policy.service.spec.ts b/src/app/core/resource-policy/resource-policy.service.spec.ts index c3f577a8e5..1c6ac47405 100644 --- a/src/app/core/resource-policy/resource-policy.service.spec.ts +++ b/src/app/core/resource-policy/resource-policy.service.spec.ts @@ -1,5 +1,4 @@ import { HttpClient } from '@angular/common/http'; -import { async } from '@angular/core/testing'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; @@ -17,7 +16,7 @@ import { FindListOptions } from '../data/request.models'; import { RequestParam } from '../cache/models/request-param.model'; import { PageInfo } from '../shared/page-info.model'; import { PaginatedList } from '../data/paginated-list'; -import { createSuccessfulRemoteDataObject } from '../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { RequestEntry } from '../data/request.reducer'; import { RestResponse } from '../cache/response.models'; diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.spec.ts b/src/app/shared/resource-policies/create/resource-policy-create.component.spec.ts index 6db1c93da1..1c41280bab 100644 --- a/src/app/shared/resource-policies/create/resource-policy-create.component.spec.ts +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.spec.ts @@ -9,17 +9,17 @@ import { TranslateModule } from '@ngx-translate/core'; import { createFailedRemoteDataObject, - createSuccessfulRemoteDataObject, - createTestComponent -} from '../../testing/utils'; + createSuccessfulRemoteDataObject +} from '../../remote-data.utils'; +import { createTestComponent } from '../../testing/utils.test'; import { ResourcePolicyCreateComponent } from './resource-policy-create.component'; import { LinkService } from '../../../core/cache/builders/link.service'; import { NotificationsService } from '../../notifications/notifications.service'; -import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; import { getMockResourcePolicyService } from '../../mocks/mock-resource-policy-service'; -import { getMockLinkService } from '../../mocks/mock-link-service'; -import { RouterStub } from '../../testing/router-stub'; +import { getMockLinkService } from '../../mocks/link-service.mock'; +import { RouterStub } from '../../testing/router.stub'; import { Item } from '../../../core/shared/item.model'; import { createMockRDPaginatedObs } from '../../../+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec'; import { ResourcePolicyEvent } from '../form/resource-policy-form.component'; @@ -27,7 +27,7 @@ import { GroupMock } from '../../testing/group-mock'; import { submittedResourcePolicy } from '../form/resource-policy-form.component.spec'; import { PolicyType } from '../../../core/resource-policy/models/policy-type.model'; import { ActionType } from '../../../core/resource-policy/models/action-type.model'; -import { EPersonMock } from '../../testing/eperson-mock'; +import { EPersonMock } from '../../testing/eperson.mock'; describe('ResourcePolicyCreateComponent test suite', () => { let comp: ResourcePolicyCreateComponent; diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.spec.ts b/src/app/shared/resource-policies/edit/resource-policy-edit.component.spec.ts index 56099aac3d..b124da0219 100644 --- a/src/app/shared/resource-policies/edit/resource-policy-edit.component.spec.ts +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.spec.ts @@ -9,16 +9,16 @@ import { TranslateModule } from '@ngx-translate/core'; import { createFailedRemoteDataObject, - createSuccessfulRemoteDataObject, - createTestComponent -} from '../../testing/utils'; + createSuccessfulRemoteDataObject +} from '../../remote-data.utils'; +import { createTestComponent } from '../../testing/utils.test'; import { LinkService } from '../../../core/cache/builders/link.service'; import { NotificationsService } from '../../notifications/notifications.service'; -import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service'; import { getMockResourcePolicyService } from '../../mocks/mock-resource-policy-service'; -import { getMockLinkService } from '../../mocks/mock-link-service'; -import { RouterStub } from '../../testing/router-stub'; +import { getMockLinkService } from '../../mocks/link-service.mock'; +import { RouterStub } from '../../testing/router.stub'; import { ResourcePolicyEvent } from '../form/resource-policy-form.component'; import { GroupMock } from '../../testing/group-mock'; import { submittedResourcePolicy } from '../form/resource-policy-form.component.spec'; diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts index fad29567d2..11d714a30e 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts @@ -6,13 +6,14 @@ import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; import { uniqueId } from 'lodash'; -import { createSuccessfulRemoteDataObject, createTestComponent } from '../../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; +import { createTestComponent } from '../../../testing/utils.test'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { RequestService } from '../../../../core/data/request.service'; -import { getMockRequestService } from '../../../mocks/mock-request.service'; +import { getMockRequestService } from '../../../mocks/request.service.mock'; import { EpersonGroupListComponent, SearchEvent } from './eperson-group-list.component'; -import { EPersonMock } from '../../../testing/eperson-mock'; +import { EPersonMock } from '../../../testing/eperson.mock'; import { GroupMock } from '../../../testing/group-mock'; import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model'; import { PaginatedList } from '../../../../core/data/paginated-list'; diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts index 5b0456e6a4..8f04948f26 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts @@ -4,7 +4,7 @@ import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; -import { createTestComponent } from '../../../../testing/utils'; +import { createTestComponent } from '../../../../testing/utils.test'; import { EpersonSearchBoxComponent } from './eperson-search-box.component'; import { SearchEvent } from '../eperson-group-list.component'; diff --git a/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.spec.ts b/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.spec.ts index b23e69c37c..bcc71a63de 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.spec.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/group-search-box/group-search-box.component.spec.ts @@ -4,7 +4,7 @@ import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; -import { createTestComponent } from '../../../../testing/utils'; +import { createTestComponent } from '../../../../testing/utils.test'; import { GroupSearchBoxComponent } from './group-search-box.component'; import { SearchEvent } from '../eperson-group-list.component'; diff --git a/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts b/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts index 46b80070b1..03978212d7 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.component.spec.ts @@ -10,24 +10,25 @@ import { TestScheduler } from 'rxjs/testing'; import { delay } from 'rxjs/operators'; import { TranslateModule } from '@ngx-translate/core'; -import { createSuccessfulRemoteDataObject, createTestComponent } from '../../testing/utils'; +import { createSuccessfulRemoteDataObject } from '../../remote-data.utils'; +import { createTestComponent } from '../../testing/utils.test'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { RequestService } from '../../../core/data/request.service'; -import { getMockRequestService } from '../../mocks/mock-request.service'; +import { getMockRequestService } from '../../mocks/request.service.mock'; import { PolicyType } from '../../../core/resource-policy/models/policy-type.model'; import { ActionType } from '../../../core/resource-policy/models/action-type.model'; import { GroupMock } from '../../testing/group-mock'; import { ResourcePolicyEvent, ResourcePolicyFormComponent } from './resource-policy-form.component'; import { FormService } from '../../form/form.service'; -import { getMockFormService } from '../../mocks/mock-form-service'; +import { getMockFormService } from '../../mocks/form-service.mock'; import { FormBuilderService } from '../../form/builder/form-builder.service'; import { EpersonGroupListComponent } from './eperson-group-list/eperson-group-list.component'; import { FormComponent } from '../../form/form.component'; import { stringToNgbDateStruct } from '../../date.util'; import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type'; -import { EPersonMock } from '../../testing/eperson-mock'; +import { EPersonMock } from '../../testing/eperson.mock'; export const mockResourcePolicyFormData = { name: [ diff --git a/src/app/shared/resource-policies/resource-policies.component.spec.ts b/src/app/shared/resource-policies/resource-policies.component.spec.ts index 4e318ca630..084d3eb82d 100644 --- a/src/app/shared/resource-policies/resource-policies.component.spec.ts +++ b/src/app/shared/resource-policies/resource-policies.component.spec.ts @@ -15,23 +15,24 @@ import { Bundle } from '../../core/shared/bundle.model'; import { createMockRDPaginatedObs } from '../../+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec'; import { Item } from '../../core/shared/item.model'; import { LinkService } from '../../core/cache/builders/link.service'; -import { getMockLinkService } from '../mocks/mock-link-service'; -import { createSuccessfulRemoteDataObject, createTestComponent } from '../testing/utils'; +import { getMockLinkService } from '../mocks/link-service.mock'; +import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; +import { createTestComponent } from '../testing/utils.test'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { NotificationsService } from '../notifications/notifications.service'; -import { NotificationsServiceStub } from '../testing/notifications-service-stub'; +import { NotificationsServiceStub } from '../testing/notifications-service.stub'; import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service'; import { getMockResourcePolicyService } from '../mocks/mock-resource-policy-service'; import { GroupDataService } from '../../core/eperson/group-data.service'; import { RequestService } from '../../core/data/request.service'; -import { getMockRequestService } from '../mocks/mock-request.service'; -import { RouterStub } from '../testing/router-stub'; +import { getMockRequestService } from '../mocks/request.service.mock'; +import { RouterStub } from '../testing/router.stub'; import { PaginatedList } from '../../core/data/paginated-list'; import { PageInfo } from '../../core/shared/page-info.model'; import { ResourcePoliciesComponent } from './resource-policies.component'; import { PolicyType } from '../../core/resource-policy/models/policy-type.model'; import { ActionType } from '../../core/resource-policy/models/action-type.model'; -import { EPersonMock } from '../testing/eperson-mock'; +import { EPersonMock } from '../testing/eperson.mock'; import { GroupMock } from '../testing/group-mock'; describe('ResourcePoliciesComponent test suite', () => { @@ -183,7 +184,6 @@ describe('ResourcePoliciesComponent test suite', () => { const pageInfo = new PageInfo(); const array = [resourcePolicy, anotherResourcePolicy]; const paginatedList = new PaginatedList(pageInfo, array); - const resourcePolicyRD = createSuccessfulRemoteDataObject(resourcePolicy); const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); beforeEach(async(() => { From e1716751d480725aac42acbda42904df50b36936 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 20 May 2020 18:10:17 +0200 Subject: [PATCH 063/103] Fixed build error --- .../resource-policies/form/resource-policy-form.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/resource-policies/form/resource-policy-form.component.ts b/src/app/shared/resource-policies/form/resource-policy-form.component.ts index 5cb3afc894..803db655d3 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.component.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.component.ts @@ -149,8 +149,9 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy { const groupRD$ = this.groupService.findByHref(this.resourcePolicy._links.group.href).pipe( getSucceededRemoteData() ); + const dsoRD$: Observable> = observableRace(epersonRD$, groupRD$); this.subs.push( - observableRace(epersonRD$, groupRD$).pipe( + dsoRD$.pipe( filter(() => this.isActive), ).subscribe((dsoRD: RemoteData) => { this.resourcePolicyGrant = dsoRD.payload; From 09ee329f17435c79caf259bbdbbbd6c2a554dc29 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 21 May 2020 12:17:37 +0200 Subject: [PATCH 064/103] Fixed failed test --- .../resource-policies.component.spec.ts | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/app/shared/resource-policies/resource-policies.component.spec.ts b/src/app/shared/resource-policies/resource-policies.component.spec.ts index 084d3eb82d..5bb7e560ff 100644 --- a/src/app/shared/resource-policies/resource-policies.component.spec.ts +++ b/src/app/shared/resource-policies/resource-policies.component.spec.ts @@ -245,6 +245,7 @@ describe('ResourcePoliciesComponent test suite', () => { comp = fixture.componentInstance; compAsAny = fixture.componentInstance; linkService.resolveLink.and.callFake((object, link) => object); + spyOn(comp, 'canDelete'); }); @@ -276,7 +277,7 @@ describe('ResourcePoliciesComponent test suite', () => { }); }); - describe('', () => { + describe('canDelete', () => { beforeEach(() => { fixture = TestBed.createComponent(ResourcePoliciesComponent); comp = fixture.componentInstance; @@ -296,29 +297,51 @@ describe('ResourcePoliciesComponent test suite', () => { fixture.destroy(); }); + it('should return false when no row is selected', () => { + expect(comp.canDelete()).toBeObservable(cold('(a|)', { + a: false + })); + }); + + it('should return true when al least is selected', () => { + const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input')); + + let event = { target: { checked: true } }; + checkbox.triggerEventHandler('change', event); + expect(comp.canDelete()).toBeObservable(cold('(a|)', { + a: true + })); + event = { target: { checked: false } }; + checkbox.triggerEventHandler('change', event); + }); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(ResourcePoliciesComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + linkService.resolveLink.and.callFake((object, link) => object); + compAsAny.isActive = true; + compAsAny.resourcePoliciesEntries$.next(resourcePolicyEntries); + resourcePolicyService.searchByResource.and.returnValue(observableOf({})); + spyOn(comp, 'initResourcePolicyLIst').and.callFake(() => ({})); + spyOn(comp, 'canDelete'); + fixture.detectChanges(); + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + it('should render a table with a row for each policy', () => { const rows = fixture.debugElement.queryAll(By.css('table > tbody > tr')); expect(rows.length).toBe(2); }); - describe('canDelete', () => { - it('should return false when no row is selected', () => { - expect(comp.canDelete()).toBeObservable(cold('(a|)', { - a: false - })); - }); - - it('should return true when al least is selected', () => { - const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input')); - - const event = { target: { checked: true } }; - checkbox.triggerEventHandler('change', event); - expect(comp.canDelete()).toBeObservable(cold('(a|)', { - a: true - })); - }); - }); - describe('deleteSelectedResourcePolicies', () => { beforeEach(() => { compAsAny.resourcePoliciesEntries$.next(resourcePolicySelectedEntries); From 51620df76cc5a73507ffe975ca906cba549b4a50 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 21 May 2020 12:18:17 +0200 Subject: [PATCH 065/103] Fixed issue with item edit page --- .../+item-page/edit-item-page/edit-item-page.routing.module.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index aa42d8ed24..87b4b7a592 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -19,6 +19,7 @@ import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/res import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; +import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service'; export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; export const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; @@ -149,6 +150,8 @@ export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations'; ]) ], providers: [ + I18nBreadcrumbResolver, + I18nBreadcrumbsService, ResourcePolicyResolver, ResourcePolicyTargetResolver ] From 8ef77df651c27d041085899690e8aa0c448c64fe Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 21 May 2020 12:19:24 +0200 Subject: [PATCH 066/103] Fixed issue with resource policies list that was not updated after a delete --- .../shared/resource-policies/resource-policies.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts index 3fb690111e..a5db9474ad 100644 --- a/src/app/shared/resource-policies/resource-policies.component.ts +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -112,6 +112,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ ngOnInit(): void { this.isActive = true; + this.requestService.removeByHrefSubstring(this.resourceUUID); this.initResourcePolicyLIst(); } @@ -134,6 +135,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ deleteSelectedResourcePolicies(): void { this.processingDelete$.next(true); + this.requestService.removeByHrefSubstring(this.resourceUUID); const policiesToDelete: ResourcePolicyCheckboxEntry[] = this.resourcePoliciesEntries$.value .filter((entry: ResourcePolicyCheckboxEntry) => entry.checked); this.subs.push( @@ -244,8 +246,6 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { * Initialize the resource's policies list */ initResourcePolicyLIst() { - // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved - this.requestService.removeByHrefSubstring(this.resourceUUID); this.resourcePolicyService.searchByResource(this.resourceUUID, null, followLink('eperson'), followLink('group')).pipe( filter(() => this.isActive), From b41acfdb3368e467c7ca4b6b38e2724b396450d7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 21 May 2020 12:21:08 +0200 Subject: [PATCH 067/103] Moved resource policy's group edit link --- .../resource-policies.component.html | 21 ++++++++++++------- src/assets/i18n/en.json5 | 4 ++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index 2a2c013ecb..07472ddbb7 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -1,4 +1,4 @@ -
+
@@ -74,17 +74,24 @@ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index d9d2221b3f..0e3810bdf9 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2133,6 +2133,10 @@ "resource-policies.table.headers.edit": "Edit", + "resource-policies.table.headers.edit.group": "Edit group", + + "resource-policies.table.headers.edit.policy": "Edit policy", + "resource-policies.table.headers.eperson": "EPerson", "resource-policies.table.headers.group": "Group", From 1dde3087ffdf0ab1e679fccca8607128c1547f85 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 22 May 2020 11:36:02 +0200 Subject: [PATCH 068/103] Fixed failed test --- .../resource-policies.component.spec.ts | 109 ++++++++---------- 1 file changed, 51 insertions(+), 58 deletions(-) diff --git a/src/app/shared/resource-policies/resource-policies.component.spec.ts b/src/app/shared/resource-policies/resource-policies.component.spec.ts index 5bb7e560ff..bab9eb4846 100644 --- a/src/app/shared/resource-policies/resource-policies.component.spec.ts +++ b/src/app/shared/resource-policies/resource-policies.component.spec.ts @@ -156,18 +156,21 @@ describe('ResourcePoliciesComponent test suite', () => { url: `url/edit` }); - const resourcePolicyEntries = [ - { - id: resourcePolicy.id, - policy: resourcePolicy, - checked: false - }, - { - id: anotherResourcePolicy.id, - policy: anotherResourcePolicy, - checked: false - } - ]; + const getInitEntries = () => { + return [ + Object.assign({}, { + id: resourcePolicy.id, + policy: resourcePolicy, + checked: false + }), + Object.assign({}, { + id: anotherResourcePolicy.id, + policy: anotherResourcePolicy, + checked: false + }) + ] + } + const resourcePolicySelectedEntries = [ { id: resourcePolicy.id, @@ -245,8 +248,6 @@ describe('ResourcePoliciesComponent test suite', () => { comp = fixture.componentInstance; compAsAny = fixture.componentInstance; linkService.resolveLink.and.callFake((object, link) => object); - spyOn(comp, 'canDelete'); - }); afterEach(() => { @@ -264,6 +265,7 @@ describe('ResourcePoliciesComponent test suite', () => { }); it('should init resource policies list properly', () => { + const expected = getInitEntries(); compAsAny.isActive = true; resourcePolicyService.searchByResource.and.returnValue(hot('a|', { a: paginatedListRD @@ -273,46 +275,7 @@ describe('ResourcePoliciesComponent test suite', () => { scheduler.schedule(() => comp.initResourcePolicyLIst()); scheduler.flush(); - expect(compAsAny.resourcePoliciesEntries$.value).toEqual(resourcePolicyEntries); - }); - }); - - describe('canDelete', () => { - beforeEach(() => { - fixture = TestBed.createComponent(ResourcePoliciesComponent); - comp = fixture.componentInstance; - compAsAny = fixture.componentInstance; - linkService.resolveLink.and.callFake((object, link) => object); - compAsAny.isActive = true; - compAsAny.resourcePoliciesEntries$.next(resourcePolicyEntries); - resourcePolicyService.searchByResource.and.returnValue(observableOf({})); - spyOn(comp, 'initResourcePolicyLIst').and.callFake(() => ({})); - fixture.detectChanges(); - }); - - afterEach(() => { - comp = null; - compAsAny = null; - de = null; - fixture.destroy(); - }); - - it('should return false when no row is selected', () => { - expect(comp.canDelete()).toBeObservable(cold('(a|)', { - a: false - })); - }); - - it('should return true when al least is selected', () => { - const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input')); - - let event = { target: { checked: true } }; - checkbox.triggerEventHandler('change', event); - expect(comp.canDelete()).toBeObservable(cold('(a|)', { - a: true - })); - event = { target: { checked: false } }; - checkbox.triggerEventHandler('change', event); + expect(compAsAny.resourcePoliciesEntries$.value).toEqual(expected); }); }); @@ -323,10 +286,10 @@ describe('ResourcePoliciesComponent test suite', () => { compAsAny = fixture.componentInstance; linkService.resolveLink.and.callFake((object, link) => object); compAsAny.isActive = true; - compAsAny.resourcePoliciesEntries$.next(resourcePolicyEntries); + const initResourcePolicyEntries = getInitEntries(); + compAsAny.resourcePoliciesEntries$.next(initResourcePolicyEntries); resourcePolicyService.searchByResource.and.returnValue(observableOf({})); spyOn(comp, 'initResourcePolicyLIst').and.callFake(() => ({})); - spyOn(comp, 'canDelete'); fixture.detectChanges(); }); @@ -337,6 +300,36 @@ describe('ResourcePoliciesComponent test suite', () => { fixture.destroy(); }); + describe('canDelete', () => { + beforeEach(() => { + const initResourcePolicyEntries = getInitEntries(); + compAsAny.resourcePoliciesEntries$.next(initResourcePolicyEntries); + fixture.detectChanges(); + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should return false when no row is selected', () => { + expect(comp.canDelete()).toBeObservable(cold('(a|)', { + a: false + })); + }); + + it('should return true when al least is selected', () => { + const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input')); + const event = { target: { checked: true } }; + checkbox.triggerEventHandler('change', event); + expect(comp.canDelete()).toBeObservable(cold('(a|)', { + a: true + })); + }); + }); + it('should render a table with a row for each policy', () => { const rows = fixture.debugElement.queryAll(By.css('table > tbody > tr')); expect(rows.length).toBe(2); @@ -372,9 +365,9 @@ describe('ResourcePoliciesComponent test suite', () => { }); it('should get the resource\'s policy list', () => { - + const initResourcePolicyEntries = getInitEntries(); expect(comp.getResourcePolicies()).toBeObservable(cold('a', { - a: resourcePolicyEntries + a: initResourcePolicyEntries })); }); From 4a65051641ea502b85eefc6e4c42192bfba5dab4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 28 Apr 2020 10:06:34 +0200 Subject: [PATCH 069/103] Fixed issue with authentication when SSR is disabled --- src/app/core/auth/auth.effects.spec.ts | 74 +++++++++++++++++---- src/app/core/auth/auth.effects.ts | 15 ++--- src/app/shared/testing/auth-service.stub.ts | 4 ++ 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 5aaced609e..094284e679 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -1,9 +1,9 @@ -import { TestBed } from '@angular/core/testing'; +import { fakeAsync, flush, TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import { Store } from '@ngrx/store'; +import { Store, StoreModule } from '@ngrx/store'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { cold, hot } from 'jasmine-marbles'; - import { Observable, of as observableOf, throwError as observableThrow } from 'rxjs'; import { AuthEffects } from './auth.effects'; @@ -29,41 +29,53 @@ import { } from './auth.actions'; import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { AuthService } from './auth.service'; -import { AuthState } from './auth.reducer'; - +import { authReducer } from './auth.reducer'; import { AuthStatus } from './models/auth-status.model'; import { EPersonMock } from '../../shared/testing/eperson.mock'; +import { AppState, storeModuleConfig } from '../../app.reducer'; +import { StoreActionTypes } from '../../store.actions'; +import { isAuthenticated, isAuthenticatedLoaded } from './selectors'; -describe('AuthEffects', () => { +fdescribe('AuthEffects', () => { let authEffects: AuthEffects; let actions: Observable; let authServiceStub; - const store: Store = jasmine.createSpyObj('store', { - /* tslint:disable:no-empty */ - dispatch: {}, - /* tslint:enable:no-empty */ - select: observableOf(true) - }); + let initialState; let token; + let store: MockStore; function init() { authServiceStub = new AuthServiceStub(); token = authServiceStub.getToken(); + initialState = { + core: { + auth: { + authenticated: false, + loaded: false, + loading: false, + authMethods: [] + } + } + }; } beforeEach(() => { init(); TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({ auth: authReducer }, storeModuleConfig) + ], providers: [ AuthEffects, + provideMockStore({ initialState }), { provide: AuthService, useValue: authServiceStub }, - { provide: Store, useValue: store }, provideMockActions(() => actions), // other providers ], }); authEffects = TestBed.get(AuthEffects); + store = TestBed.get(Store); }); describe('authenticate$', () => { @@ -362,4 +374,40 @@ describe('AuthEffects', () => { }); }) }); + + describe('clearInvalidTokenOnRehydrate$', () => { + + beforeEach(() => { + store.overrideSelector(isAuthenticated, false); + }); + + describe('when auth loaded is false', () => { + it('should not call removeToken method', (done) => { + store.overrideSelector(isAuthenticatedLoaded, false); + actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } }); + spyOn(authServiceStub, 'removeToken'); + + authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => { + expect(authServiceStub.removeToken).not.toHaveBeenCalled(); + + }); + + done(); + }); + }); + + describe('when auth loaded is true', () => { + it('should call removeToken method', fakeAsync(() => { + store.overrideSelector(isAuthenticatedLoaded, true); + actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } }); + spyOn(authServiceStub, 'removeToken'); + + authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => { + expect(authServiceStub.removeToken).toHaveBeenCalled(); + flush(); + }); + + })); + }); + }); }); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 717aaff01e..c6d447961a 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,20 +1,18 @@ -import { Observable, of as observableOf } from 'rxjs'; - -import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; +import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators'; // import @ngrx import { Actions, Effect, ofType } from '@ngrx/effects'; import { Action, select, Store } from '@ngrx/store'; // import services import { AuthService } from './auth.service'; - import { EPerson } from '../eperson/models/eperson.model'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; import { AppState } from '../../app.reducer'; -import { isAuthenticated } from './selectors'; +import { isAuthenticated, isAuthenticatedLoaded } from './selectors'; import { StoreActionTypes } from '../../store.actions'; import { AuthMethod } from './models/auth.method'; // import actions @@ -187,10 +185,11 @@ export class AuthEffects { public clearInvalidTokenOnRehydrate$: Observable = this.actions$.pipe( ofType(StoreActionTypes.REHYDRATE), switchMap(() => { - return this.store.pipe( - select(isAuthenticated), + const isLoaded$ = this.store.pipe(select(isAuthenticatedLoaded)); + const authenticated$ = this.store.pipe(select(isAuthenticated)); + return observableCombineLatest(isLoaded$, authenticated$).pipe( take(1), - filter((authenticated) => !authenticated), + filter(([loaded, authenticated]) => loaded && !authenticated), tap(() => this.authService.removeToken()), tap(() => this.authService.resetAuthenticationError()) ); diff --git a/src/app/shared/testing/auth-service.stub.ts b/src/app/shared/testing/auth-service.stub.ts index 31143bc856..7e7e70a754 100644 --- a/src/app/shared/testing/auth-service.stub.ts +++ b/src/app/shared/testing/auth-service.stub.ts @@ -150,4 +150,8 @@ export class AuthServiceStub { getImpersonateID() { return this.impersonating; } + + resetAuthenticationError() { + return; + } } From b61490341e6f3a1e3bb98e49cd3921ed0153a7d9 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 22 May 2020 16:50:06 +0200 Subject: [PATCH 070/103] Added missing class in the authentication form to apply style properly --- .../log-in/methods/password/log-in-password.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/log-in/methods/password/log-in-password.component.html b/src/app/shared/log-in/methods/password/log-in-password.component.html index ddd5083d44..16f42a1e16 100644 --- a/src/app/shared/log-in/methods/password/log-in-password.component.html +++ b/src/app/shared/log-in/methods/password/log-in-password.component.html @@ -1,4 +1,5 @@ - Date: Fri, 22 May 2020 16:50:48 +0200 Subject: [PATCH 071/103] fixed test --- src/app/core/auth/auth.effects.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 094284e679..c08615ecc9 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -36,7 +36,7 @@ import { AppState, storeModuleConfig } from '../../app.reducer'; import { StoreActionTypes } from '../../store.actions'; import { isAuthenticated, isAuthenticatedLoaded } from './selectors'; -fdescribe('AuthEffects', () => { +describe('AuthEffects', () => { let authEffects: AuthEffects; let actions: Observable; let authServiceStub; From 5f6fdf00ec3e3fa71439d577129d80298c7b5c2a Mon Sep 17 00:00:00 2001 From: Reeta Kuuskoski Date: Mon, 25 May 2020 12:56:34 +0300 Subject: [PATCH 072/103] Add Finnish translations (first set after master rebase) --- src/assets/i18n/fi.json5 | 2226 +++++++++++++------------------------- 1 file changed, 739 insertions(+), 1487 deletions(-) diff --git a/src/assets/i18n/fi.json5 b/src/assets/i18n/fi.json5 index d7afa97f08..a8556c4684 100644 --- a/src/assets/i18n/fi.json5 +++ b/src/assets/i18n/fi.json5 @@ -1,236 +1,179 @@ { // "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", - // TODO New key - Add a translation - "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", + "404.help": "Hakemaasi sivua ei löytynyt. Sivu on voitu siirtää tai poistaa. Painamalla alapuolella olevaa nappia palaat kotisivulle. ", // "404.link.home-page": "Take me to the home page", - // TODO New key - Add a translation - "404.link.home-page": "Take me to the home page", + "404.link.home-page": "Palaa kotisivulle", // "404.page-not-found": "page not found", - // TODO New key - Add a translation - "404.page-not-found": "page not found", + "404.page-not-found": "sivua ei löytynyt", // "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", + "admin.registries.bitstream-formats.create.failure.content": "Virhe uutta tiedostoformaattia luotaessa.", // "admin.registries.bitstream-formats.create.failure.head": "Failure", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.failure.head": "Failure", + "admin.registries.bitstream-formats.create.failure.head": "Virhe", // "admin.registries.bitstream-formats.create.head": "Create Bitstream format", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.head": "Create Bitstream format", + "admin.registries.bitstream-formats.create.head": "Luo tiedostoformaatti", // "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", + "admin.registries.bitstream-formats.create.new": "Lisää uusi tiedostoformaatti", // "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", + "admin.registries.bitstream-formats.create.success.content": "Uusi tiedostoformaatti luotu.", // "admin.registries.bitstream-formats.create.success.head": "Success", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.success.head": "Success", + "admin.registries.bitstream-formats.create.success.head": "Valmis", // "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", + "admin.registries.bitstream-formats.delete.failure.amount": "{{ amount }} formaatin poisto epäonnistui", // "admin.registries.bitstream-formats.delete.failure.head": "Failure", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.failure.head": "Failure", + "admin.registries.bitstream-formats.delete.failure.head": "Virhe", // "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", + "admin.registries.bitstream-formats.delete.success.amount": "Poistettu {{ amount }} formaatti(a)", // "admin.registries.bitstream-formats.delete.success.head": "Success", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.success.head": "Success", + "admin.registries.bitstream-formats.delete.success.head": "Valmis", // "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", + "admin.registries.bitstream-formats.description": "Tässä luetellaan tiedostoformaatit ja niiden tukitasot.", // "admin.registries.bitstream-formats.edit.description.hint": "", - // TODO New key - Add a translation "admin.registries.bitstream-formats.edit.description.hint": "", // "admin.registries.bitstream-formats.edit.description.label": "Description", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.description.label": "Description", + "admin.registries.bitstream-formats.edit.description.label": "Kuvaus", // "admin.registries.bitstream-formats.edit.extensions.hint": "Extensions are file extensions that are used to automatically identify the format of uploaded files. You can enter several extensions for each format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.extensions.hint": "Extensions are file extensions that are used to automatically identify the format of uploaded files. You can enter several extensions for each format.", + "admin.registries.bitstream-formats.edit.extensions.hint": "Tarkenteet ovat tiedostopäätteitä, joita käytetään tallennettujen tiedostojen formaatin automaattiseen tunnistamiseen. Yhtä formaattia voi vastata useampi tiedostopääte.", // "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", + "admin.registries.bitstream-formats.edit.extensions.label": "Tiedostopäätteet", // "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extension without the dot", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extension without the dot", + "admin.registries.bitstream-formats.edit.extensions.placeholder": "Lisää tiedostopääte ilman pistettä", // "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", + "admin.registries.bitstream-formats.edit.failure.content": "Virhe tiedostoformaattia muokattaessa.", // "admin.registries.bitstream-formats.edit.failure.head": "Failure", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.failure.head": "Failure", + "admin.registries.bitstream-formats.edit.failure.head": "Virhe", // "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", + "admin.registries.bitstream-formats.edit.head": "Tiedostoformaatti: {{ format }}", // "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are hidden from the user, and used for administrative purposes.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are hidden from the user, and used for administrative purposes.", + "admin.registries.bitstream-formats.edit.internal.hint": "Sisäisiksi merkittyjä formaatteja käytetään hallinnollisiin tarkoituksiin, ja ne on piilotettu käyttäjiltä.", // "admin.registries.bitstream-formats.edit.internal.label": "Internal", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.internal.label": "Internal", + "admin.registries.bitstream-formats.edit.internal.label": "Sisäinen", // "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", + "admin.registries.bitstream-formats.edit.mimetype.hint": "Tiedostoformaatin MIME-tyyppi. MIME-tyypin ei tarvitse olla yksilöllinen.", // "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", + "admin.registries.bitstream-formats.edit.mimetype.label": "MIME-tyyppi", // "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", + "admin.registries.bitstream-formats.edit.shortDescription.hint": "Formaatin yksilöllinen nimi (esim. Microsoft Word XP tai Microsoft Word 2000)", // "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", + "admin.registries.bitstream-formats.edit.shortDescription.label": "Nimi", // "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", + "admin.registries.bitstream-formats.edit.success.content": "Tiedostoformaattia muokattu.", // "admin.registries.bitstream-formats.edit.success.head": "Success", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.success.head": "Success", + "admin.registries.bitstream-formats.edit.success.head": "Valmis", // "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", + "admin.registries.bitstream-formats.edit.supportLevel.hint": "Tukitaso, jonka järjestelmää ylläpitävä instituutio takaa tiedostoformaatille.", // "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", + "admin.registries.bitstream-formats.edit.supportLevel.label": "Tukitaso", // "admin.registries.bitstream-formats.head": "Bitstream Format Registry", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.head": "Bitstream Format Registry", + "admin.registries.bitstream-formats.head": "Tiedostoformaattirekisteri", // "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", + "admin.registries.bitstream-formats.no-items": "Ei tiedostoformaatteja.", // "admin.registries.bitstream-formats.table.delete": "Delete selected", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.delete": "Delete selected", + "admin.registries.bitstream-formats.table.delete": "Poista valittu", // "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", + "admin.registries.bitstream-formats.table.deselect-all": "Poista kaikkien valinta", // "admin.registries.bitstream-formats.table.internal": "internal", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.internal": "internal", + "admin.registries.bitstream-formats.table.internal": "sisäinen", // "admin.registries.bitstream-formats.table.mimetype": "MIME Type", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.mimetype": "MIME Type", + "admin.registries.bitstream-formats.table.mimetype": "MIME-tyyppi", // "admin.registries.bitstream-formats.table.name": "Name", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.name": "Name", + "admin.registries.bitstream-formats.table.name": "Nimi", // "admin.registries.bitstream-formats.table.return": "Return", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.return": "Return", + "admin.registries.bitstream-formats.table.return": "Paluu", // "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", + "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Tunnettu", // "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", + "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Tuettu", // "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", + "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Tuntematon", // "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", + "admin.registries.bitstream-formats.table.supportLevel.head": "Tukitaso", // "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", + "admin.registries.bitstream-formats.title": "DSpace Angular :: Tiedostoformaattirekisteri", // "admin.registries.metadata.description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.", - // TODO New key - Add a translation - "admin.registries.metadata.description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.", + "admin.registries.metadata.description": "Metadatarekisteriin on koottu tässä julkaisuarkistossa käytössä olevat metadatakentät. Kentät voivat jakautua eri skeemoihin. DSpace-alusta edellyttää Qualified Dublin Core -skeeman käyttöä.", // "admin.registries.metadata.form.create": "Create metadata schema", - // TODO New key - Add a translation - "admin.registries.metadata.form.create": "Create metadata schema", + "admin.registries.metadata.form.create": "Luo metadataskeema", // "admin.registries.metadata.form.edit": "Edit metadata schema", - // TODO New key - Add a translation - "admin.registries.metadata.form.edit": "Edit metadata schema", + "admin.registries.metadata.form.edit": "Muokkaa metadataskeemaa", // "admin.registries.metadata.form.name": "Name", - // TODO New key - Add a translation - "admin.registries.metadata.form.name": "Name", + "admin.registries.metadata.form.name": "Nimi", // "admin.registries.metadata.form.namespace": "Namespace", - // TODO New key - Add a translation - "admin.registries.metadata.form.namespace": "Namespace", + "admin.registries.metadata.form.namespace": "Nimiavaruus", // "admin.registries.metadata.head": "Metadata Registry", - // TODO New key - Add a translation - "admin.registries.metadata.head": "Metadata Registry", + "admin.registries.metadata.head": "Metadatarekisteri", // "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", + "admin.registries.metadata.schemas.no-items": "Ei metadataskeemoja.", // "admin.registries.metadata.schemas.table.delete": "Delete selected", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.table.delete": "Delete selected", + "admin.registries.metadata.schemas.table.delete": "Poista valittu", // "admin.registries.metadata.schemas.table.id": "ID", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.table.id": "ID", + "admin.registries.metadata.schemas.table.id": "ID-tunnus", // "admin.registries.metadata.schemas.table.name": "Name", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.table.name": "Name", + "admin.registries.metadata.schemas.table.name": "Nimi", // "admin.registries.metadata.schemas.table.namespace": "Namespace", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.table.namespace": "Namespace", + "admin.registries.metadata.schemas.table.namespace": "Nimiavaruus", // "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", - // TODO New key - Add a translation - "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", + "admin.registries.metadata.title": "DSpace Angular :: Metadatarekisteri", @@ -239,96 +182,77 @@ "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", // "admin.registries.schema.fields.head": "Schema metadata fields", - // TODO New key - Add a translation - "admin.registries.schema.fields.head": "Schema metadata fields", + "admin.registries.schema.fields.head": "Skeeman metadatakentät", // "admin.registries.schema.fields.no-items": "No metadata fields to show.", - // TODO New key - Add a translation - "admin.registries.schema.fields.no-items": "No metadata fields to show.", + "admin.registries.schema.fields.no-items": "Ei metadatakenttiä.", // "admin.registries.schema.fields.table.delete": "Delete selected", - // TODO New key - Add a translation - "admin.registries.schema.fields.table.delete": "Delete selected", + "admin.registries.schema.fields.table.delete": "Poista valittu", // "admin.registries.schema.fields.table.field": "Field", - // TODO New key - Add a translation - "admin.registries.schema.fields.table.field": "Field", + "admin.registries.schema.fields.table.field": "Kenttä", // "admin.registries.schema.fields.table.scopenote": "Scope Note", - // TODO New key - Add a translation - "admin.registries.schema.fields.table.scopenote": "Scope Note", + "admin.registries.schema.fields.table.scopenote": "Soveltamisala", // "admin.registries.schema.form.create": "Create metadata field", - // TODO New key - Add a translation - "admin.registries.schema.form.create": "Create metadata field", + "admin.registries.schema.form.create": "Luo metadatakenttä", // "admin.registries.schema.form.edit": "Edit metadata field", - // TODO New key - Add a translation - "admin.registries.schema.form.edit": "Edit metadata field", + "admin.registries.schema.form.edit": "Muokkaa metadatakenttää", // "admin.registries.schema.form.element": "Element", - // TODO New key - Add a translation - "admin.registries.schema.form.element": "Element", + "admin.registries.schema.form.element": "Elementti", // "admin.registries.schema.form.qualifier": "Qualifier", - // TODO New key - Add a translation - "admin.registries.schema.form.qualifier": "Qualifier", + "admin.registries.schema.form.qualifier": "Tarkenne", // "admin.registries.schema.form.scopenote": "Scope Note", - // TODO New key - Add a translation - "admin.registries.schema.form.scopenote": "Scope Note", + "admin.registries.schema.form.scopenote": "Soveltamisala", // "admin.registries.schema.head": "Metadata Schema", - // TODO New key - Add a translation - "admin.registries.schema.head": "Metadata Schema", + "admin.registries.schema.head": "Metadataskeema", // "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", // TODO New key - Add a translation "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", // "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", - // TODO New key - Add a translation - "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", + "admin.registries.schema.notification.deleted.failure": "{{amount}} metadataskeeman poisto epäonnistui ", // "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", - // TODO New key - Add a translation - "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", + "admin.registries.schema.notification.deleted.success": "{{amount}} metadataskeemaa poistettu", // "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", // TODO New key - Add a translation "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", // "admin.registries.schema.notification.failure": "Error", - // TODO New key - Add a translation - "admin.registries.schema.notification.failure": "Error", + "admin.registries.schema.notification.failure": "Virhe", // "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", // TODO New key - Add a translation "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", // "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", + "admin.registries.schema.notification.field.deleted.failure": "{{amount}} metadatakentän poisto epäonnistui", // "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", + "admin.registries.schema.notification.field.deleted.success": "{{amount}} metadatakenttää poistettu", // "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", // TODO New key - Add a translation "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", // "admin.registries.schema.notification.success": "Success", - // TODO New key - Add a translation - "admin.registries.schema.notification.success": "Success", + "admin.registries.schema.notification.success": "Valmis", // "admin.registries.schema.return": "Return", - // TODO New key - Add a translation - "admin.registries.schema.return": "Return", + "admin.registries.schema.return": "Paluu", // "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", - // TODO New key - Add a translation - "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", + "admin.registries.schema.title": "DSpace Angular :: Metadataskeemarekisteri", @@ -784,12 +708,10 @@ // "auth.errors.invalid-user": "Invalid email address or password.", - // TODO New key - Add a translation - "auth.errors.invalid-user": "Invalid email address or password.", + "auth.errors.invalid-user": "Virheellinen sähköpostiosoite tai salasana.", // "auth.messages.expired": "Your session has expired. Please log in again.", - // TODO New key - Add a translation - "auth.messages.expired": "Your session has expired. Please log in again.", + "auth.messages.expired": "Istuntosi on vanhentunut. Kirjaudu uudelleen.", @@ -864,44 +786,34 @@ // "browse.comcol.by.author": "By Author", - // TODO New key - Add a translation - "browse.comcol.by.author": "By Author", + "browse.comcol.by.author": "Tekijän mukaan", // "browse.comcol.by.dateissued": "By Issue Date", - // TODO New key - Add a translation - "browse.comcol.by.dateissued": "By Issue Date", + "browse.comcol.by.dateissued": "Julkaisuajan mukaan", // "browse.comcol.by.subject": "By Subject", - // TODO New key - Add a translation - "browse.comcol.by.subject": "By Subject", + "browse.comcol.by.subject": "Avainsanan mukaan", // "browse.comcol.by.title": "By Title", - // TODO New key - Add a translation - "browse.comcol.by.title": "By Title", + "browse.comcol.by.title": "Nimekkeen mukaan", // "browse.comcol.head": "Browse", - // TODO New key - Add a translation - "browse.comcol.head": "Browse", + "browse.comcol.head": "Selaa", // "browse.empty": "No items to show.", - // TODO New key - Add a translation - "browse.empty": "No items to show.", + "browse.empty": "Ei tietueita.", // "browse.metadata.author": "Author", - // TODO New key - Add a translation - "browse.metadata.author": "Author", + "browse.metadata.author": "Tekijä", // "browse.metadata.dateissued": "Issue Date", - // TODO New key - Add a translation - "browse.metadata.dateissued": "Issue Date", + "browse.metadata.dateissued": "Julkaisuaika", // "browse.metadata.subject": "Subject", - // TODO New key - Add a translation - "browse.metadata.subject": "Subject", + "browse.metadata.subject": "Aihe", // "browse.metadata.title": "Title", - // TODO New key - Add a translation - "browse.metadata.title": "Title", + "browse.metadata.title": "Nimeke", // "browse.metadata.author.breadcrumbs": "Browse by Author", // TODO New key - Add a translation @@ -920,123 +832,95 @@ "browse.metadata.title.breadcrumbs": "Browse by Title", // "browse.startsWith.choose_start": "(Choose start)", - // TODO New key - Add a translation - "browse.startsWith.choose_start": "(Choose start)", + "browse.startsWith.choose_start": "(Valitse alku)", // "browse.startsWith.choose_year": "(Choose year)", - // TODO New key - Add a translation - "browse.startsWith.choose_year": "(Choose year)", + "browse.startsWith.choose_year": "(Valitse vuosi)", // "browse.startsWith.jump": "Jump to a point in the index:", - // TODO New key - Add a translation - "browse.startsWith.jump": "Jump to a point in the index:", + "browse.startsWith.jump": "Hyppää indeksin kohtaan:", // "browse.startsWith.months.april": "April", - // TODO New key - Add a translation - "browse.startsWith.months.april": "April", + "browse.startsWith.months.april": "Huhtikuu", // "browse.startsWith.months.august": "August", - // TODO New key - Add a translation - "browse.startsWith.months.august": "August", + "browse.startsWith.months.august": "Elokuu", // "browse.startsWith.months.december": "December", - // TODO New key - Add a translation - "browse.startsWith.months.december": "December", + "browse.startsWith.months.december": "Joulukuu", // "browse.startsWith.months.february": "February", - // TODO New key - Add a translation - "browse.startsWith.months.february": "February", + "browse.startsWith.months.february": "Helmikuu", // "browse.startsWith.months.january": "January", - // TODO New key - Add a translation - "browse.startsWith.months.january": "January", + "browse.startsWith.months.january": "Tammikuu", // "browse.startsWith.months.july": "July", - // TODO New key - Add a translation - "browse.startsWith.months.july": "July", + "browse.startsWith.months.july": "Heinäkuu", // "browse.startsWith.months.june": "June", - // TODO New key - Add a translation - "browse.startsWith.months.june": "June", + "browse.startsWith.months.june": "Kesäkuu", // "browse.startsWith.months.march": "March", - // TODO New key - Add a translation - "browse.startsWith.months.march": "March", + "browse.startsWith.months.march": "Maaliskuu", // "browse.startsWith.months.may": "May", - // TODO New key - Add a translation - "browse.startsWith.months.may": "May", + "browse.startsWith.months.may": "Toukokuu", // "browse.startsWith.months.none": "(Choose month)", - // TODO New key - Add a translation - "browse.startsWith.months.none": "(Choose month)", + "browse.startsWith.months.none": "(Valitse kuukausi)", // "browse.startsWith.months.november": "November", - // TODO New key - Add a translation - "browse.startsWith.months.november": "November", + "browse.startsWith.months.november": "Marraskuu", // "browse.startsWith.months.october": "October", - // TODO New key - Add a translation - "browse.startsWith.months.october": "October", + "browse.startsWith.months.october": "Lokakuu", // "browse.startsWith.months.september": "September", - // TODO New key - Add a translation - "browse.startsWith.months.september": "September", + "browse.startsWith.months.september": "Syyskuu", // "browse.startsWith.submit": "Go", - // TODO New key - Add a translation - "browse.startsWith.submit": "Go", + "browse.startsWith.submit": "Käynnistä", // "browse.startsWith.type_date": "Or type in a date (year-month):", - // TODO New key - Add a translation - "browse.startsWith.type_date": "Or type in a date (year-month):", + "browse.startsWith.type_date": "Tai anna päiväys (vuosi-kuukausi):", // "browse.startsWith.type_text": "Or enter first few letters:", - // TODO New key - Add a translation - "browse.startsWith.type_text": "Or enter first few letters:", + "browse.startsWith.type_text": "Tai anna muutama alkukirjain:", // "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", - // TODO New key - Add a translation - "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", + "browse.title": "Selataan {{ collection }}-kokoelmaa {{ field }}-kentän arvolla {{ value }}", // "chips.remove": "Remove chip", - // TODO New key - Add a translation - "chips.remove": "Remove chip", + "chips.remove": "Poista chip", // "collection.create.head": "Create a Collection", - // TODO New key - Add a translation - "collection.create.head": "Create a Collection", + "collection.create.head": "Luo kokoelma", // "collection.create.notifications.success": "Successfully created the Collection", // TODO New key - Add a translation "collection.create.notifications.success": "Successfully created the Collection", // "collection.create.sub-head": "Create a Collection for Community {{ parent }}", - // TODO New key - Add a translation - "collection.create.sub-head": "Create a Collection for Community {{ parent }}", + "collection.create.sub-head": "Luo kokoelma {{ parent }}-yhteisöön", // "collection.delete.cancel": "Cancel", - // TODO New key - Add a translation - "collection.delete.cancel": "Cancel", + "collection.delete.cancel": "Peruuta", // "collection.delete.confirm": "Confirm", - // TODO New key - Add a translation - "collection.delete.confirm": "Confirm", + "collection.delete.confirm": "Vahvista", // "collection.delete.head": "Delete Collection", - // TODO New key - Add a translation - "collection.delete.head": "Delete Collection", + "collection.delete.head": "Poista kokoelma", // "collection.delete.notification.fail": "Collection could not be deleted", - // TODO New key - Add a translation - "collection.delete.notification.fail": "Collection could not be deleted", + "collection.delete.notification.fail": "Kokoelman poisto epäonnistui", // "collection.delete.notification.success": "Successfully deleted collection", - // TODO New key - Add a translation - "collection.delete.notification.success": "Successfully deleted collection", + "collection.delete.notification.success": "Kokoelma poistettu", // "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", // TODO New key - Add a translation @@ -1045,12 +929,10 @@ // "collection.edit.delete": "Delete this collection", - // TODO New key - Add a translation - "collection.edit.delete": "Delete this collection", + "collection.edit.delete": "Poista kokoelma", // "collection.edit.head": "Edit Collection", - // TODO New key - Add a translation - "collection.edit.head": "Edit Collection", + "collection.edit.head": "Muokkaa kokoelmaa", // "collection.edit.breadcrumbs": "Edit Collection", // TODO New key - Add a translation @@ -1059,72 +941,56 @@ // "collection.edit.item-mapper.cancel": "Cancel", - // TODO New key - Add a translation - "collection.edit.item-mapper.cancel": "Cancel", + "collection.edit.item-mapper.cancel": "Peruuta", // "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", // TODO New key - Add a translation "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", // "collection.edit.item-mapper.confirm": "Map selected items", - // TODO New key - Add a translation - "collection.edit.item-mapper.confirm": "Map selected items", + "collection.edit.item-mapper.confirm": "Liitä valitut tietueet", // "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", + "collection.edit.item-mapper.description": "Tällä työkalulla kokoelmien ylläpitäjät voivat liittää tietueita muista kokoelmista tähän kokoelmaan. Voit etsiä tietueita muista kokoelmista ja liittää ne tähän kokoelmaan tai selata luetteloa tähän kokoelmaan liitetyistä tietueista.", // "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", - // TODO New key - Add a translation - "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", + "collection.edit.item-mapper.head": "Tietueliitosväline - Liitä tietueita muista kokoelmista", // "collection.edit.item-mapper.no-search": "Please enter a query to search", - // TODO New key - Add a translation - "collection.edit.item-mapper.no-search": "Please enter a query to search", + "collection.edit.item-mapper.no-search": "Anna hakulauseke", // "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", + "collection.edit.item-mapper.notifications.map.error.content": "Virheitä liitettäessä {{amount}} tietuetta.", // "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", + "collection.edit.item-mapper.notifications.map.error.head": "Virheitä liitoksissa", // "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", + "collection.edit.item-mapper.notifications.map.success.content": "Liitetty {{amount}} tietuetta.", // "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", + "collection.edit.item-mapper.notifications.map.success.head": "Liitos valmis", // "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", + "collection.edit.item-mapper.notifications.unmap.error.content": "Virheitä {{amount}} tietueen liitoksia poistettaessa.", // "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", + "collection.edit.item-mapper.notifications.unmap.error.head": "Poista virheelliset liitokset", // "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", + "collection.edit.item-mapper.notifications.unmap.success.content": "Poistettu {{amount}} tietueen liitokset.", // "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", + "collection.edit.item-mapper.notifications.unmap.success.head": "Liitosten poisto valmis", // "collection.edit.item-mapper.remove": "Remove selected item mappings", - // TODO New key - Add a translation - "collection.edit.item-mapper.remove": "Remove selected item mappings", + "collection.edit.item-mapper.remove": "Poista valitut tietueliitokset", // "collection.edit.item-mapper.tabs.browse": "Browse mapped items", - // TODO New key - Add a translation - "collection.edit.item-mapper.tabs.browse": "Browse mapped items", + "collection.edit.item-mapper.tabs.browse": "Selaa liitettyjä tietueita", // "collection.edit.item-mapper.tabs.map": "Map new items", - // TODO New key - Add a translation - "collection.edit.item-mapper.tabs.map": "Map new items", + "collection.edit.item-mapper.tabs.map": "Liitä uusia tietueita", @@ -1267,72 +1133,56 @@ // "collection.form.abstract": "Short Description", - // TODO New key - Add a translation - "collection.form.abstract": "Short Description", + "collection.form.abstract": "Lyhyt kuvaus", // "collection.form.description": "Introductory text (HTML)", - // TODO New key - Add a translation - "collection.form.description": "Introductory text (HTML)", + "collection.form.description": "Johdantoteksti (HTML)", // "collection.form.errors.title.required": "Please enter a collection name", - // TODO New key - Add a translation - "collection.form.errors.title.required": "Please enter a collection name", + "collection.form.errors.title.required": "Anna kokoelman nimi", // "collection.form.license": "License", - // TODO New key - Add a translation - "collection.form.license": "License", + "collection.form.license": "Lisenssi", // "collection.form.provenance": "Provenance", - // TODO New key - Add a translation - "collection.form.provenance": "Provenance", + "collection.form.provenance": "Provenanssi", // "collection.form.rights": "Copyright text (HTML)", - // TODO New key - Add a translation - "collection.form.rights": "Copyright text (HTML)", + "collection.form.rights": "Tekijänoikeusteksti (HTML)", // "collection.form.tableofcontents": "News (HTML)", - // TODO New key - Add a translation - "collection.form.tableofcontents": "News (HTML)", + "collection.form.tableofcontents": "Uutiset (HTML)", // "collection.form.title": "Name", - // TODO New key - Add a translation - "collection.form.title": "Name", + "collection.form.title": "Nimi", // "collection.page.browse.recent.head": "Recent Submissions", - // TODO New key - Add a translation - "collection.page.browse.recent.head": "Recent Submissions", + "collection.page.browse.recent.head": "Viimeksi lisätyt", // "collection.page.browse.recent.empty": "No items to show", - // TODO New key - Add a translation "collection.page.browse.recent.empty": "No items to show", // "collection.page.handle": "Permanent URI for this collection", - // TODO New key - Add a translation - "collection.page.handle": "Permanent URI for this collection", + "collection.page.handle": "Kokoelman pysyvä URL-osoite", // "collection.page.license": "License", - // TODO New key - Add a translation - "collection.page.license": "License", + "collection.page.license": "Lisenssi", // "collection.page.news": "News", - // TODO New key - Add a translation - "collection.page.news": "News", + "collection.page.news": "Uutiset", // "collection.select.confirm": "Confirm selected", - // TODO New key - Add a translation - "collection.select.confirm": "Confirm selected", + "collection.select.confirm": "Vahvista valinta", // "collection.select.empty": "No collections to show", - // TODO New key - Add a translation - "collection.select.empty": "No collections to show", + "collection.select.empty": "Ei kokoelmia", // "collection.select.table.title": "Title", - // TODO New key - Add a translation - "collection.select.table.title": "Title", + "collection.select.table.title": "Nimeke", @@ -1361,48 +1211,39 @@ // "community.create.head": "Create a Community", - // TODO New key - Add a translation - "community.create.head": "Create a Community", + "community.create.head": "Luo yhteisö", // "community.create.notifications.success": "Successfully created the Community", // TODO New key - Add a translation "community.create.notifications.success": "Successfully created the Community", // "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", - // TODO New key - Add a translation - "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", + "community.create.sub-head": "Luo alayhteisö {{ parent }}-yhteisölle", // "community.delete.cancel": "Cancel", - // TODO New key - Add a translation - "community.delete.cancel": "Cancel", + "community.delete.cancel": "Peruuta", // "community.delete.confirm": "Confirm", - // TODO New key - Add a translation - "community.delete.confirm": "Confirm", + "community.delete.confirm": "Vahvista", // "community.delete.head": "Delete Community", - // TODO New key - Add a translation - "community.delete.head": "Delete Community", + "community.delete.head": "Poista yhteisö", // "community.delete.notification.fail": "Community could not be deleted", - // TODO New key - Add a translation - "community.delete.notification.fail": "Community could not be deleted", + "community.delete.notification.fail": "Yhteisön poisto epäonnistui", // "community.delete.notification.success": "Successfully deleted community", - // TODO New key - Add a translation - "community.delete.notification.success": "Successfully deleted community", + "community.delete.notification.success": "Yhteisö poistettu", // "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", // TODO New key - Add a translation "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", // "community.edit.delete": "Delete this community", - // TODO New key - Add a translation - "community.edit.delete": "Delete this community", + "community.edit.delete": "Poista tämä yhteisö", // "community.edit.head": "Edit Community", - // TODO New key - Add a translation - "community.edit.head": "Edit Community", + "community.edit.head": "Muokkaa yhteisöä", // "community.edit.breadcrumbs": "Edit Community", // TODO New key - Add a translation @@ -1476,94 +1317,72 @@ // "community.form.abstract": "Short Description", - // TODO New key - Add a translation - "community.form.abstract": "Short Description", + "community.form.abstract": "Lyhyt kuvaus", // "community.form.description": "Introductory text (HTML)", - // TODO New key - Add a translation - "community.form.description": "Introductory text (HTML)", + "community.form.description": "Johdantoteksti (HTML)", // "community.form.errors.title.required": "Please enter a community name", - // TODO New key - Add a translation - "community.form.errors.title.required": "Please enter a community name", + "community.form.errors.title.required": "Anna kokoelman nimi", // "community.form.rights": "Copyright text (HTML)", - // TODO New key - Add a translation - "community.form.rights": "Copyright text (HTML)", + "community.form.rights": "Tekijänoikeusteksti (HTML)", // "community.form.tableofcontents": "News (HTML)", - // TODO New key - Add a translation - "community.form.tableofcontents": "News (HTML)", + "community.form.tableofcontents": "Uutiset (HTML)", // "community.form.title": "Name", - // TODO New key - Add a translation - "community.form.title": "Name", + "community.form.title": "Nimi", // "community.page.handle": "Permanent URI for this community", - // TODO New key - Add a translation - "community.page.handle": "Permanent URI for this community", + "community.page.handle": "Yhteisön pysyvä URL-osoite", // "community.page.license": "License", - // TODO New key - Add a translation - "community.page.license": "License", + "community.page.license": "Lisenssi", // "community.page.news": "News", - // TODO New key - Add a translation - "community.page.news": "News", + "community.page.news": "Uutiset", // "community.all-lists.head": "Subcommunities and Collections", - // TODO New key - Add a translation - "community.all-lists.head": "Subcommunities and Collections", + "community.all-lists.head": "Alayhteisöt ja kokoelmat", // "community.sub-collection-list.head": "Collections of this Community", - // TODO New key - Add a translation - "community.sub-collection-list.head": "Collections of this Community", + "community.sub-collection-list.head": "Yhteisön kokoelmat", // "community.sub-community-list.head": "Communities of this Community", - // TODO New key - Add a translation - "community.sub-community-list.head": "Communities of this Community", + "community.sub-community-list.head": "Yhteisön alayhteisöt", // "dso-selector.create.collection.head": "New collection", - // TODO New key - Add a translation - "dso-selector.create.collection.head": "New collection", + "dso-selector.create.collection.head": "Uusi kokoelma", // "dso-selector.create.community.head": "New community", - // TODO New key - Add a translation - "dso-selector.create.community.head": "New community", + "dso-selector.create.community.head": "Uusi yhteisö", // "dso-selector.create.community.sub-level": "Create a new community in", - // TODO New key - Add a translation - "dso-selector.create.community.sub-level": "Create a new community in", + "dso-selector.create.community.sub-level": "Luo uusi yhteisö julkaisuarkistoon", // "dso-selector.create.community.top-level": "Create a new top-level community", - // TODO New key - Add a translation - "dso-selector.create.community.top-level": "Create a new top-level community", + "dso-selector.create.community.top-level": "Luo uusi ylätason yhteisö", // "dso-selector.create.item.head": "New item", - // TODO New key - Add a translation - "dso-selector.create.item.head": "New item", + "dso-selector.create.item.head": "Uusi tietue", // "dso-selector.edit.collection.head": "Edit collection", - // TODO New key - Add a translation - "dso-selector.edit.collection.head": "Edit collection", + "dso-selector.edit.collection.head": "Muokkaa kokoelmaa", // "dso-selector.edit.community.head": "Edit community", - // TODO New key - Add a translation - "dso-selector.edit.community.head": "Edit community", + "dso-selector.edit.community.head": "Muokkaa yhteisöä", // "dso-selector.edit.item.head": "Edit item", - // TODO New key - Add a translation - "dso-selector.edit.item.head": "Edit item", + "dso-selector.edit.item.head": "Muokkaa tietuetta", // "dso-selector.no-results": "No {{ type }} found", - // TODO New key - Add a translation - "dso-selector.no-results": "No {{ type }} found", + "dso-selector.no-results": "Ei {{ type }}-tyyppiä.", // "dso-selector.placeholder": "Search for a {{ type }}", - // TODO New key - Add a translation - "dso-selector.placeholder": "Search for a {{ type }}", + "dso-selector.placeholder": "Hae {{ type }}", @@ -1572,72 +1391,55 @@ "error.bitstream": "Error fetching bitstream", // "error.browse-by": "Error fetching items", - // TODO New key - Add a translation - "error.browse-by": "Error fetching items", + "error.browse-by": "Virhe tietueita noudettaessa", // "error.collection": "Error fetching collection", - // TODO New key - Add a translation - "error.collection": "Error fetching collection", + "error.collection": "Virhe kokoelmaa noudettaessa", // "error.collections": "Error fetching collections", - // TODO New key - Add a translation - "error.collections": "Error fetching collections", + "error.collections": "Virhe kokoelmia noudettaessa", // "error.community": "Error fetching community", - // TODO New key - Add a translation - "error.community": "Error fetching community", + "error.community": "Virhe yhteisöä noudettaessa", // "error.identifier": "No item found for the identifier", - // TODO New key - Add a translation - "error.identifier": "No item found for the identifier", + "error.identifier": "Ei tunnistetta vastaavaa tietuetta", // "error.default": "Error", - // TODO New key - Add a translation - "error.default": "Error", + "error.default": "Virhe", // "error.item": "Error fetching item", - // TODO New key - Add a translation - "error.item": "Error fetching item", + "error.item": "Virhe tietuetta noudettaessa", // "error.items": "Error fetching items", - // TODO New key - Add a translation - "error.items": "Error fetching items", + "error.items": "Virhe tietueita noudettaessa", // "error.objects": "Error fetching objects", - // TODO New key - Add a translation - "error.objects": "Error fetching objects", + "error.objects": "Virhe kohteita noudettaessa", // "error.recent-submissions": "Error fetching recent submissions", - // TODO New key - Add a translation - "error.recent-submissions": "Error fetching recent submissions", + "error.recent-submissions": "Virhe viimeksi lisättyjä julkaisuja noudettaessa", // "error.search-results": "Error fetching search results", - // TODO New key - Add a translation - "error.search-results": "Error fetching search results", + "error.search-results": "Virhe hakutuloksia noudettaessa", // "error.sub-collections": "Error fetching sub-collections", - // TODO New key - Add a translation - "error.sub-collections": "Error fetching sub-collections", + "error.sub-collections": "Virhe alakokoelmia noudettaessa", // "error.sub-communities": "Error fetching sub-communities", - // TODO New key - Add a translation - "error.sub-communities": "Error fetching sub-communities", + "error.sub-communities": "Virhe alayhteisöjä noudettaessa", // "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

", - // TODO New key - Add a translation - "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

", + "error.submission.sections.init-form-error": "Virhe osiota alustettaessa, tarkista syöttölomakkeesi asetukset. Lisätietoja alla:

", // "error.top-level-communities": "Error fetching top-level communities", - // TODO New key - Add a translation - "error.top-level-communities": "Error fetching top-level communities", + "error.top-level-communities": "Virhe ylätason yhteisöjä noudettaessa", // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - // TODO New key - Add a translation - "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", + "error.validation.license.notgranted": "Julkaisuprosessia ei voi päättää, ellet hyväksy julkaisulisenssiä. Voit myös tallentaa tiedot ja jatkaa tallennusta myöhemmin tai poistaa kaikki syöttämäsi tiedot.", // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", - // TODO New key - Add a translation - "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", + "error.validation.pattern": "Syötteen on noudatettava seuraavaa kaavaa: {{ pattern }}.", // "error.validation.filerequired": "The file upload is mandatory", // TODO New key - Add a translation @@ -1646,15 +1448,12 @@ // "footer.copyright": "copyright © 2002-{{ year }}", - // TODO New key - Add a translation - "footer.copyright": "copyright © 2002-{{ year }}", + "footer.copyright": "tekijänoikeus © 2002-{{ year }}", // "footer.link.dspace": "DSpace software", - // TODO New key - Add a translation - "footer.link.dspace": "DSpace software", + "footer.link.dspace": "DSpace-ohjelmisto", // "footer.link.duraspace": "DuraSpace", - // TODO New key - Add a translation "footer.link.duraspace": "DuraSpace", @@ -1667,52 +1466,40 @@ "form.add-help": "Click here to add the current entry and to add another one", // "form.cancel": "Cancel", - // TODO New key - Add a translation - "form.cancel": "Cancel", + "form.cancel": "Peruuta", // "form.clear": "Clear", - // TODO New key - Add a translation - "form.clear": "Clear", + "form.clear": "Tyhjennä", // "form.clear-help": "Click here to remove the selected value", - // TODO New key - Add a translation - "form.clear-help": "Click here to remove the selected value", + "form.clear-help": "Napauta tästä poistaaksesi valitun arvon", // "form.edit": "Edit", - // TODO New key - Add a translation - "form.edit": "Edit", + "form.edit": "Muokkaa", // "form.edit-help": "Click here to edit the selected value", - // TODO New key - Add a translation - "form.edit-help": "Click here to edit the selected value", + "form.edit-help": "Napauta tästä muokataksesi valittua arvoa", // "form.first-name": "First name", - // TODO New key - Add a translation - "form.first-name": "First name", + "form.first-name": "Etunimi", // "form.group-collapse": "Collapse", - // TODO New key - Add a translation - "form.group-collapse": "Collapse", + "form.group-collapse": "Sulje", // "form.group-collapse-help": "Click here to collapse", - // TODO New key - Add a translation - "form.group-collapse-help": "Click here to collapse", + "form.group-collapse-help": "Sulje napauttamalla", // "form.group-expand": "Expand", - // TODO New key - Add a translation - "form.group-expand": "Expand", + "form.group-expand": "Laajenna", // "form.group-expand-help": "Click here to expand and add more elements", - // TODO New key - Add a translation - "form.group-expand-help": "Click here to expand and add more elements", + "form.group-expand-help": "Avaa napauttamalla lisätäksesi uusia elementtejä", // "form.last-name": "Last name", - // TODO New key - Add a translation - "form.last-name": "Last name", + "form.last-name": "Sukunimi", // "form.loading": "Loading...", - // TODO New key - Add a translation - "form.loading": "Loading...", + "form.loading": "Ladataan...", // "form.lookup": "Lookup", // TODO New key - Add a translation @@ -1723,58 +1510,45 @@ "form.lookup-help": "Click here to look up an existing relation", // "form.no-results": "No results found", - // TODO New key - Add a translation - "form.no-results": "No results found", + "form.no-results": "Ei tuloksia", // "form.no-value": "No value entered", - // TODO New key - Add a translation - "form.no-value": "No value entered", + "form.no-value": "Ei syötettyä arvoa", // "form.other-information": {}, - // TODO New key - Add a translation "form.other-information": {}, // "form.remove": "Remove", - // TODO New key - Add a translation - "form.remove": "Remove", + "form.remove": "Poista", // "form.save": "Save", - // TODO New key - Add a translation - "form.save": "Save", + "form.save": "Tallenna", // "form.save-help": "Save changes", - // TODO New key - Add a translation - "form.save-help": "Save changes", + "form.save-help": "Tallenna muutokset", // "form.search": "Search", - // TODO New key - Add a translation - "form.search": "Search", + "form.search": "Hae", // "form.search-help": "Click here to looking for an existing correspondence", - // TODO New key - Add a translation - "form.search-help": "Click here to looking for an existing correspondence", + "form.search-help": "Valitse etsiäksesi olemassa olevaa vastaavuutta", // "form.submit": "Submit", - // TODO New key - Add a translation - "form.submit": "Submit", + "form.submit": "Lähetä", // "home.description": "", - // TODO New key - Add a translation "home.description": "", // "home.title": "DSpace Angular :: Home", - // TODO New key - Add a translation - "home.title": "DSpace Angular :: Home", + "home.title": "DSpace Angular :: Etusivu", // "home.top-level-communities.head": "Communities in DSpace", - // TODO New key - Add a translation - "home.top-level-communities.head": "Communities in DSpace", + "home.top-level-communities.head": "Julkaisuarkiston yhteisöt", // "home.top-level-communities.help": "Select a community to browse its collections.", - // TODO New key - Add a translation - "home.top-level-communities.help": "Select a community to browse its collections.", + "home.top-level-communities.help": "Valitse yhteisö, jonka kokoelmia haluat selata.", @@ -1948,32 +1722,25 @@ // "item.edit.delete.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.delete.cancel": "Cancel", + "item.edit.delete.cancel": "Peruuta", // "item.edit.delete.confirm": "Delete", - // TODO New key - Add a translation - "item.edit.delete.confirm": "Delete", + "item.edit.delete.confirm": "Poista", // "item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.", - // TODO New key - Add a translation - "item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.", + "item.edit.delete.description": "Haluatko varmasti poistaa tiedoston pysyvästi?", // "item.edit.delete.error": "An error occurred while deleting the item", - // TODO New key - Add a translation - "item.edit.delete.error": "An error occurred while deleting the item", + "item.edit.delete.error": "Virhe tietuetta poistettaessa", // "item.edit.delete.header": "Delete item: {{ id }}", - // TODO New key - Add a translation - "item.edit.delete.header": "Delete item: {{ id }}", + "item.edit.delete.header": "Poista tietue: {{ id }}", // "item.edit.delete.success": "The item has been deleted", - // TODO New key - Add a translation - "item.edit.delete.success": "The item has been deleted", + "item.edit.delete.success": "Tietue poistettu", // "item.edit.head": "Edit Item", - // TODO New key - Add a translation - "item.edit.head": "Edit Item", + "item.edit.head": "Muokkaa tietuetta", // "item.edit.breadcrumbs": "Edit Item", // TODO New key - Add a translation @@ -1982,472 +1749,360 @@ // "item.edit.item-mapper.buttons.add": "Map item to selected collections", - // TODO New key - Add a translation - "item.edit.item-mapper.buttons.add": "Map item to selected collections", + "item.edit.item-mapper.buttons.add": "Liitä tietue valittuihin kokoelmiin", // "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", - // TODO New key - Add a translation - "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", + "item.edit.item-mapper.buttons.remove": "Poista tietueen liitos valituista kokoelmista", // "item.edit.item-mapper.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.item-mapper.cancel": "Cancel", + "item.edit.item-mapper.cancel": "Peruuta", // "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", - // TODO New key - Add a translation - "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", + "item.edit.item-mapper.description": "Tällä työkalulla kokoelmien ylläpitäjät voivat liittää tietueen muihin kokoelmiin. Voit hakea kokoelmia ja liittää aineiston niihin tai selata luetteloa kokoelmista, joihin aineisto on liitetty.", // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", - // TODO New key - Add a translation - "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", + "item.edit.item-mapper.head": "Tietueliitosväline - Liitä tietue kokoelmiin", // "item.edit.item-mapper.item": "Item: \"{{name}}\"", // TODO New key - Add a translation "item.edit.item-mapper.item": "Item: \"{{name}}\"", // "item.edit.item-mapper.no-search": "Please enter a query to search", - // TODO New key - Add a translation - "item.edit.item-mapper.no-search": "Please enter a query to search", + "item.edit.item-mapper.no-search": "Anna hakulauseke", // "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", + "item.edit.item-mapper.notifications.add.error.content": "Virhe liitettäessä tietuetta {{amount}} kokoelmaan.", // "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", + "item.edit.item-mapper.notifications.add.error.head": "Virhe liitettäessä", // "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", + "item.edit.item-mapper.notifications.add.success.content": "Tietue liitetty {{amount}} kokoelmaan.", // "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", + "item.edit.item-mapper.notifications.add.success.head": "Liitos valmis", // "item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.", + "item.edit.item-mapper.notifications.remove.error.content": "Virheitä poistettaessa liitosta {{amount}} kokoelmaan.", // "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", + "item.edit.item-mapper.notifications.remove.error.head": "Virheellisten liitosten poisto", // "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", + "item.edit.item-mapper.notifications.remove.success.content": "Poistettu tietueen liitos {{amount}} kokoelmasta.", // "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", + "item.edit.item-mapper.notifications.remove.success.head": "Liitos poistettu", // "item.edit.item-mapper.tabs.browse": "Browse mapped collections", - // TODO New key - Add a translation - "item.edit.item-mapper.tabs.browse": "Browse mapped collections", + "item.edit.item-mapper.tabs.browse": "Selaa liitettyjä kokoelmia", // "item.edit.item-mapper.tabs.map": "Map new collections", - // TODO New key - Add a translation - "item.edit.item-mapper.tabs.map": "Map new collections", + "item.edit.item-mapper.tabs.map": "Liitä uusia kokoelmia", // "item.edit.metadata.add-button": "Add", - // TODO New key - Add a translation - "item.edit.metadata.add-button": "Add", + "item.edit.metadata.add-button": "Lisää", // "item.edit.metadata.discard-button": "Discard", - // TODO New key - Add a translation - "item.edit.metadata.discard-button": "Discard", + "item.edit.metadata.discard-button": "Hylkää", // "item.edit.metadata.edit.buttons.edit": "Edit", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.edit": "Edit", + "item.edit.metadata.edit.buttons.edit": "Muokkaa", // "item.edit.metadata.edit.buttons.remove": "Remove", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.remove": "Remove", + "item.edit.metadata.edit.buttons.remove": "Poista", // "item.edit.metadata.edit.buttons.undo": "Undo changes", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.undo": "Undo changes", + "item.edit.metadata.edit.buttons.undo": "Kumoa muutokset", // "item.edit.metadata.edit.buttons.unedit": "Stop editing", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.unedit": "Stop editing", + "item.edit.metadata.edit.buttons.unedit": "Lopeta muokkaus", // "item.edit.metadata.headers.edit": "Edit", - // TODO New key - Add a translation - "item.edit.metadata.headers.edit": "Edit", + "item.edit.metadata.headers.edit": "Muokkaa", // "item.edit.metadata.headers.field": "Field", - // TODO New key - Add a translation - "item.edit.metadata.headers.field": "Field", + "item.edit.metadata.headers.field": "Kenttä", // "item.edit.metadata.headers.language": "Lang", - // TODO New key - Add a translation - "item.edit.metadata.headers.language": "Lang", + "item.edit.metadata.headers.language": "Kieli", // "item.edit.metadata.headers.value": "Value", - // TODO New key - Add a translation - "item.edit.metadata.headers.value": "Value", + "item.edit.metadata.headers.value": "Arvo", // "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", - // TODO New key - Add a translation - "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", + "item.edit.metadata.metadatafield.invalid": "Valitse oikea metadatakenttä", // "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - // TODO New key - Add a translation - "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + "item.edit.metadata.notifications.discarded.content": "Muutokset hylätty. Valitse 'Kumoa' palauttaaksesi muutokset", // "item.edit.metadata.notifications.discarded.title": "Changed discarded", - // TODO New key - Add a translation - "item.edit.metadata.notifications.discarded.title": "Changed discarded", + "item.edit.metadata.notifications.discarded.title": "Muutokset hylätty", // "item.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", - // TODO New key - Add a translation - "item.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", + "item.edit.metadata.notifications.invalid.content": "Muutoksia ei tallennettu. Tarkista kaikkien kenttien oikeellisuus ennen tallennusta.", // "item.edit.metadata.notifications.invalid.title": "Metadata invalid", - // TODO New key - Add a translation - "item.edit.metadata.notifications.invalid.title": "Metadata invalid", + "item.edit.metadata.notifications.invalid.title": "Virheellinen metadata", // "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - // TODO New key - Add a translation - "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", + "item.edit.metadata.notifications.outdated.content": "Toinen käyttäjä on muuttanut parhaillaan muokkaamaasi tietuetta. Tekemäsi muutokset on hylätty ristiriitojen estämiseksi", // "item.edit.metadata.notifications.outdated.title": "Changed outdated", - // TODO New key - Add a translation - "item.edit.metadata.notifications.outdated.title": "Changed outdated", + "item.edit.metadata.notifications.outdated.title": "Muutokset vanhentuneet", // "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", - // TODO New key - Add a translation - "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", + "item.edit.metadata.notifications.saved.content": "Muutokset tietueen metadataan tallennettu.", // "item.edit.metadata.notifications.saved.title": "Metadata saved", - // TODO New key - Add a translation - "item.edit.metadata.notifications.saved.title": "Metadata saved", + "item.edit.metadata.notifications.saved.title": "Metadata tallennettu", // "item.edit.metadata.reinstate-button": "Undo", - // TODO New key - Add a translation - "item.edit.metadata.reinstate-button": "Undo", + "item.edit.metadata.reinstate-button": "Peruuta", // "item.edit.metadata.save-button": "Save", - // TODO New key - Add a translation - "item.edit.metadata.save-button": "Save", + "item.edit.metadata.save-button": "Tallenna", // "item.edit.modify.overview.field": "Field", - // TODO New key - Add a translation - "item.edit.modify.overview.field": "Field", + "item.edit.modify.overview.field": "Kenttä", // "item.edit.modify.overview.language": "Language", - // TODO New key - Add a translation - "item.edit.modify.overview.language": "Language", + "item.edit.modify.overview.language": "Kieli", // "item.edit.modify.overview.value": "Value", - // TODO New key - Add a translation - "item.edit.modify.overview.value": "Value", + "item.edit.modify.overview.value": "Arvo", // "item.edit.move.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.move.cancel": "Cancel", + "item.edit.move.cancel": "Peruuta", // "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", - // TODO New key - Add a translation - "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", + "item.edit.move.description": "Valitse kokoelma, johon haluat siirtää tietueen. Voit antaa hakulausekkeen kokoelmien määrän pienentämiseksi.", // "item.edit.move.error": "An error occurred when attempting to move the item", - // TODO New key - Add a translation - "item.edit.move.error": "An error occurred when attempting to move the item", + "item.edit.move.error": "Virhe tietuetta siirrettäessä", // "item.edit.move.head": "Move item: {{id}}", - // TODO New key - Add a translation - "item.edit.move.head": "Move item: {{id}}", + "item.edit.move.head": "Siirrä tietue: {{id}}", // "item.edit.move.inheritpolicies.checkbox": "Inherit policies", - // TODO New key - Add a translation - "item.edit.move.inheritpolicies.checkbox": "Inherit policies", + "item.edit.move.inheritpolicies.checkbox": "Peri auktorisointisäännöt", // "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", - // TODO New key - Add a translation - "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", + "item.edit.move.inheritpolicies.description": "Peri kohdekokoelman oletusauktorisointisäännöt", // "item.edit.move.move": "Move", - // TODO New key - Add a translation - "item.edit.move.move": "Move", + "item.edit.move.move": "Siirrä", // "item.edit.move.processing": "Moving...", - // TODO New key - Add a translation - "item.edit.move.processing": "Moving...", + "item.edit.move.processing": "Siirretään...", // "item.edit.move.search.placeholder": "Enter a search query to look for collections", - // TODO New key - Add a translation - "item.edit.move.search.placeholder": "Enter a search query to look for collections", + "item.edit.move.search.placeholder": "Anna hakulauseke kokoelmien etsimiseksi", // "item.edit.move.success": "The item has been moved successfully", - // TODO New key - Add a translation - "item.edit.move.success": "The item has been moved successfully", + "item.edit.move.success": "Tietue siirretty", // "item.edit.move.title": "Move item", - // TODO New key - Add a translation - "item.edit.move.title": "Move item", + "item.edit.move.title": "Siirrä tietue", // "item.edit.private.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.private.cancel": "Cancel", + "item.edit.private.cancel": "Peruuta", // "item.edit.private.confirm": "Make it Private", - // TODO New key - Add a translation - "item.edit.private.confirm": "Make it Private", + "item.edit.private.confirm": "Muuta yksityiseksi", // "item.edit.private.description": "Are you sure this item should be made private in the archive?", - // TODO New key - Add a translation - "item.edit.private.description": "Are you sure this item should be made private in the archive?", + "item.edit.private.description": "Oletko varma, että haluat muuttaa tietueen yksityiseksi?", // "item.edit.private.error": "An error occurred while making the item private", - // TODO New key - Add a translation - "item.edit.private.error": "An error occurred while making the item private", + "item.edit.private.error": "Virhe muutettaessa tietuetta yksityiseksi", // "item.edit.private.header": "Make item private: {{ id }}", - // TODO New key - Add a translation - "item.edit.private.header": "Make item private: {{ id }}", + "item.edit.private.header": "Muuta yksityiseksi tietue: {{ id }}", // "item.edit.private.success": "The item is now private", - // TODO New key - Add a translation - "item.edit.private.success": "The item is now private", + "item.edit.private.success": "Tietue on yksityinen", // "item.edit.public.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.public.cancel": "Cancel", + "item.edit.public.cancel": "Peruuta", // "item.edit.public.confirm": "Make it Public", - // TODO New key - Add a translation - "item.edit.public.confirm": "Make it Public", + "item.edit.public.confirm": "Muuta julkiseksi", // "item.edit.public.description": "Are you sure this item should be made public in the archive?", - // TODO New key - Add a translation - "item.edit.public.description": "Are you sure this item should be made public in the archive?", + "item.edit.public.description": "Oletko varma, että haluat muuttaa tietueen julkiseksi?", // "item.edit.public.error": "An error occurred while making the item public", - // TODO New key - Add a translation - "item.edit.public.error": "An error occurred while making the item public", + "item.edit.public.error": "Virhe muutettaessa tietuetta julkiseksi", // "item.edit.public.header": "Make item public: {{ id }}", - // TODO New key - Add a translation - "item.edit.public.header": "Make item public: {{ id }}", + "item.edit.public.header": "Muuta julkiseksi tietue: {{ id }}", // "item.edit.public.success": "The item is now public", - // TODO New key - Add a translation - "item.edit.public.success": "The item is now public", + "item.edit.public.success": "Tietue on julkinen", // "item.edit.reinstate.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.reinstate.cancel": "Cancel", + "item.edit.reinstate.cancel": "Peruuta", // "item.edit.reinstate.confirm": "Reinstate", - // TODO New key - Add a translation - "item.edit.reinstate.confirm": "Reinstate", + "item.edit.reinstate.confirm": "Palauta", // "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", - // TODO New key - Add a translation - "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", + "item.edit.reinstate.description": "Oletko varma, että haluat palauttaa tietueen käyttöön?", // "item.edit.reinstate.error": "An error occurred while reinstating the item", - // TODO New key - Add a translation - "item.edit.reinstate.error": "An error occurred while reinstating the item", + "item.edit.reinstate.error": "Virhe palautettaessa tietuetta käyttöön", // "item.edit.reinstate.header": "Reinstate item: {{ id }}", - // TODO New key - Add a translation - "item.edit.reinstate.header": "Reinstate item: {{ id }}", + "item.edit.reinstate.header": "Palauta käyttöön tietue: {{ id }}", // "item.edit.reinstate.success": "The item was reinstated successfully", - // TODO New key - Add a translation - "item.edit.reinstate.success": "The item was reinstated successfully", + "item.edit.reinstate.success": "Tietue palautettu käyttöön", // "item.edit.relationships.discard-button": "Discard", - // TODO New key - Add a translation - "item.edit.relationships.discard-button": "Discard", + "item.edit.relationships.discard-button": "Hylkää", // "item.edit.relationships.edit.buttons.remove": "Remove", - // TODO New key - Add a translation - "item.edit.relationships.edit.buttons.remove": "Remove", + "item.edit.relationships.edit.buttons.remove": "Poista", // "item.edit.relationships.edit.buttons.undo": "Undo changes", - // TODO New key - Add a translation - "item.edit.relationships.edit.buttons.undo": "Undo changes", + "item.edit.relationships.edit.buttons.undo": "Peruuta muutokset", // "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - // TODO New key - Add a translation - "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + "item.edit.relationships.notifications.discarded.content": "Muutoksesi hylättiin. Valitse 'Peruuta' palauttaaksesi ne.", // "item.edit.relationships.notifications.discarded.title": "Changes discarded", - // TODO New key - Add a translation - "item.edit.relationships.notifications.discarded.title": "Changes discarded", + "item.edit.relationships.notifications.discarded.title": "Muutokset hylätty", // "item.edit.relationships.notifications.failed.title": "Error deleting relationship", - // TODO New key - Add a translation - "item.edit.relationships.notifications.failed.title": "Error deleting relationship", + "item.edit.relationships.notifications.failed.title": "Virhe yhteyksiä poistettaessa", // "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - // TODO New key - Add a translation - "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", + "item.edit.relationships.notifications.outdated.content": "Toinen käyttäjä on muuttanut parhaillaan muokkaamaasi tietuetta. Tekemäsi muutokset on hylätty ristiriitojen estämiseksi", // "item.edit.relationships.notifications.outdated.title": "Changes outdated", - // TODO New key - Add a translation - "item.edit.relationships.notifications.outdated.title": "Changes outdated", + "item.edit.relationships.notifications.outdated.title": "Muutokset vanhentuneet", // "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", - // TODO New key - Add a translation - "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", + "item.edit.relationships.notifications.saved.content": "Muutokset tietueen yhteyksiin tallennettu.", // "item.edit.relationships.notifications.saved.title": "Relationships saved", - // TODO New key - Add a translation - "item.edit.relationships.notifications.saved.title": "Relationships saved", + "item.edit.relationships.notifications.saved.title": "Yhteydet tallennettu", // "item.edit.relationships.reinstate-button": "Undo", - // TODO New key - Add a translation - "item.edit.relationships.reinstate-button": "Undo", + "item.edit.relationships.reinstate-button": "Peruuta", // "item.edit.relationships.save-button": "Save", - // TODO New key - Add a translation - "item.edit.relationships.save-button": "Save", + "item.edit.relationships.save-button": "Tallenna", // "item.edit.tabs.bitstreams.head": "Bitstreams", - // TODO New key - Add a translation - "item.edit.tabs.bitstreams.head": "Bitstreams", + "item.edit.tabs.bitstreams.head": "Tietueen tiedostot", // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", - // TODO New key - Add a translation - "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", + "item.edit.tabs.bitstreams.title": "Tietueen muokkaus - Tiedostot", // "item.edit.tabs.curate.head": "Curate", - // TODO New key - Add a translation - "item.edit.tabs.curate.head": "Curate", + "item.edit.tabs.curate.head": "Kuratoi", // "item.edit.tabs.curate.title": "Item Edit - Curate", - // TODO New key - Add a translation - "item.edit.tabs.curate.title": "Item Edit - Curate", + "item.edit.tabs.curate.title": "Tietueen muokkaus - Kuratointi", // "item.edit.tabs.metadata.head": "Metadata", - // TODO New key - Add a translation - "item.edit.tabs.metadata.head": "Metadata", + "item.edit.tabs.metadata.head": "Tietueen metadata", // "item.edit.tabs.metadata.title": "Item Edit - Metadata", - // TODO New key - Add a translation - "item.edit.tabs.metadata.title": "Item Edit - Metadata", + "item.edit.tabs.metadata.title": "Tietueen muokkaus - Metadata", // "item.edit.tabs.relationships.head": "Relationships", - // TODO New key - Add a translation - "item.edit.tabs.relationships.head": "Relationships", + "item.edit.tabs.relationships.head": "Tietueen yhteydet", // "item.edit.tabs.relationships.title": "Item Edit - Relationships", - // TODO New key - Add a translation - "item.edit.tabs.relationships.title": "Item Edit - Relationships", + "item.edit.tabs.relationships.title": "Tietueen muokkaus - Yhteydet", // "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", + "item.edit.tabs.status.buttons.authorizations.button": "Käyttöoikeudet...", // "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", + "item.edit.tabs.status.buttons.authorizations.label": "Muokkaa tietueen käyttöoikeussääntöjä", // "item.edit.tabs.status.buttons.delete.button": "Permanently delete", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.delete.button": "Permanently delete", + "item.edit.tabs.status.buttons.delete.button": "Poista pysyvästi", // "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", + "item.edit.tabs.status.buttons.delete.label": "Poista tietue kokonaan", // "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", + "item.edit.tabs.status.buttons.mappedCollections.button": "Liitetyt kokoelmat", // "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", + "item.edit.tabs.status.buttons.mappedCollections.label": "Hallinnoi liitettyjä kokoelmia", // "item.edit.tabs.status.buttons.move.button": "Move...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.move.button": "Move...", + "item.edit.tabs.status.buttons.move.button": "Siirrä...", // "item.edit.tabs.status.buttons.move.label": "Move item to another collection", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.move.label": "Move item to another collection", + "item.edit.tabs.status.buttons.move.label": "Siirrä tietue toiseen kokoelmaan", // "item.edit.tabs.status.buttons.private.button": "Make it private...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.private.button": "Make it private...", + "item.edit.tabs.status.buttons.private.button": "Muuta yksityiseksi...", // "item.edit.tabs.status.buttons.private.label": "Make item private", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.private.label": "Make item private", + "item.edit.tabs.status.buttons.private.label": "Muuta tietue yksityiseksi", // "item.edit.tabs.status.buttons.public.button": "Make it public...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.public.button": "Make it public...", + "item.edit.tabs.status.buttons.public.button": "Muuta julkiseksi...", // "item.edit.tabs.status.buttons.public.label": "Make item public", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.public.label": "Make item public", + "item.edit.tabs.status.buttons.public.label": "Muuta tietue julkiseksi", // "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", + "item.edit.tabs.status.buttons.reinstate.button": "Palauta käyttöön...", // "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", + "item.edit.tabs.status.buttons.reinstate.label": "Palauta tietue arkistoon", // "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", + "item.edit.tabs.status.buttons.withdraw.button": "Poista käytöstä...", // "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", + "item.edit.tabs.status.buttons.withdraw.label": "Poista tietue käytöstä", // "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", - // TODO New key - Add a translation - "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", + "item.edit.tabs.status.description": "Tervetuloa tietueen hallintasivulle. Täällä voit poistaa käytöstä, palauttaa käyttöön, siirtää tai poistaa tietueen. Voit myös päivittää tai lisätä uutta metadataa / tiedostoja muilla välilehdillä.", // "item.edit.tabs.status.head": "Status", - // TODO New key - Add a translation - "item.edit.tabs.status.head": "Status", + "item.edit.tabs.status.head": "Tietueen tila", // "item.edit.tabs.status.labels.handle": "Handle", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.handle": "Handle", + "item.edit.tabs.status.labels.handle": "Handle-tunnus", // "item.edit.tabs.status.labels.id": "Item Internal ID", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.id": "Item Internal ID", + "item.edit.tabs.status.labels.id": "Tietueen sisäinen ID-tunnus", // "item.edit.tabs.status.labels.itemPage": "Item Page", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.itemPage": "Item Page", + "item.edit.tabs.status.labels.itemPage": "Tietueen tiedot", // "item.edit.tabs.status.labels.lastModified": "Last Modified", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.lastModified": "Last Modified", + "item.edit.tabs.status.labels.lastModified": "Viimeksi muokattu", // "item.edit.tabs.status.title": "Item Edit - Status", - // TODO New key - Add a translation - "item.edit.tabs.status.title": "Item Edit - Status", + "item.edit.tabs.status.title": "Tietueen muokkaus - Tila", // "item.edit.tabs.versionhistory.head": "Version History", // TODO New key - Add a translation @@ -2462,108 +2117,83 @@ "item.edit.tabs.versionhistory.under-construction": "Editing or adding new versions is not yet possible in this user interface.", // "item.edit.tabs.view.head": "View Item", - // TODO New key - Add a translation - "item.edit.tabs.view.head": "View Item", + "item.edit.tabs.view.head": "Näytä tietue", // "item.edit.tabs.view.title": "Item Edit - View", - // TODO New key - Add a translation - "item.edit.tabs.view.title": "Item Edit - View", + "item.edit.tabs.view.title": "Tietueen muokkaus - Näytä", // "item.edit.withdraw.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.withdraw.cancel": "Cancel", + "item.edit.withdraw.cancel": "Peruuta", // "item.edit.withdraw.confirm": "Withdraw", - // TODO New key - Add a translation - "item.edit.withdraw.confirm": "Withdraw", + "item.edit.withdraw.confirm": "Poista käytöstä", // "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", - // TODO New key - Add a translation - "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", + "item.edit.withdraw.description": "Oletko varma tietueen käytöstä poistamisesta?", // "item.edit.withdraw.error": "An error occurred while withdrawing the item", - // TODO New key - Add a translation - "item.edit.withdraw.error": "An error occurred while withdrawing the item", + "item.edit.withdraw.error": "Virhe tietuetta käytöstä poistettaessa", // "item.edit.withdraw.header": "Withdraw item: {{ id }}", - // TODO New key - Add a translation - "item.edit.withdraw.header": "Withdraw item: {{ id }}", + "item.edit.withdraw.header": "Poistettu käytöstä tietue: {{ id }}", // "item.edit.withdraw.success": "The item was withdrawn successfully", - // TODO New key - Add a translation - "item.edit.withdraw.success": "The item was withdrawn successfully", + "item.edit.withdraw.success": "Tietue poistettu käytöstä", // "item.page.abstract": "Abstract", - // TODO New key - Add a translation - "item.page.abstract": "Abstract", + "item.page.abstract": "Tiivistelmä", // "item.page.author": "Authors", - // TODO New key - Add a translation - "item.page.author": "Authors", + "item.page.author": "Tekijät", // "item.page.citation": "Citation", - // TODO New key - Add a translation - "item.page.citation": "Citation", + "item.page.citation": "Viittaus", // "item.page.collections": "Collections", - // TODO New key - Add a translation - "item.page.collections": "Collections", + "item.page.collections": "Kokoelmat", // "item.page.date": "Date", - // TODO New key - Add a translation - "item.page.date": "Date", + "item.page.date": "Päivämäärä", // "item.page.files": "Files", - // TODO New key - Add a translation - "item.page.files": "Files", + "item.page.files": "Tiedostot", // "item.page.filesection.description": "Description:", - // TODO New key - Add a translation - "item.page.filesection.description": "Description:", + "item.page.filesection.description": "Kuvaus:", // "item.page.filesection.download": "Download", - // TODO New key - Add a translation - "item.page.filesection.download": "Download", + "item.page.filesection.download": "Lataa", // "item.page.filesection.format": "Format:", - // TODO New key - Add a translation - "item.page.filesection.format": "Format:", + "item.page.filesection.format": "Formaatti:", // "item.page.filesection.name": "Name:", - // TODO New key - Add a translation - "item.page.filesection.name": "Name:", + "item.page.filesection.name": "Nimi:", // "item.page.filesection.size": "Size:", - // TODO New key - Add a translation - "item.page.filesection.size": "Size:", + "item.page.filesection.size": "Koko:", // "item.page.journal.search.title": "Articles in this journal", - // TODO New key - Add a translation - "item.page.journal.search.title": "Articles in this journal", + "item.page.journal.search.title": "Artikkelit tässä julkaisussa", // "item.page.link.full": "Full item page", - // TODO New key - Add a translation - "item.page.link.full": "Full item page", + "item.page.link.full": "Tietueen kaikki tiedot", // "item.page.link.simple": "Simple item page", - // TODO New key - Add a translation - "item.page.link.simple": "Simple item page", + "item.page.link.simple": "Tietueen suppeat tiedot", // "item.page.person.search.title": "Articles by this author", - // TODO New key - Add a translation - "item.page.person.search.title": "Articles by this author", + "item.page.person.search.title": "Tekijän artikkelit", // "item.page.related-items.view-more": "Show {{ amount }} more", - // TODO New key - Add a translation - "item.page.related-items.view-more": "Show {{ amount }} more", + "item.page.related-items.view-more": "Näytä lisää", // "item.page.related-items.view-less": "Hide last {{ amount }}", - // TODO New key - Add a translation - "item.page.related-items.view-less": "Hide last {{ amount }}", + "item.page.related-items.view-less": "Näytä vähemmän", // "item.page.relationships.isAuthorOfPublication": "Publications", // TODO New key - Add a translation @@ -2582,34 +2212,27 @@ "item.page.relationships.isOrgUnitOfProject": "Research Projects", // "item.page.subject": "Keywords", - // TODO New key - Add a translation - "item.page.subject": "Keywords", + "item.page.subject": "Avainsanat", // "item.page.uri": "URI", - // TODO New key - Add a translation - "item.page.uri": "URI", + "item.page.uri": "URL-osoite", // "item.select.confirm": "Confirm selected", - // TODO New key - Add a translation - "item.select.confirm": "Confirm selected", + "item.select.confirm": "Vahvista valinta", // "item.select.empty": "No items to show", - // TODO New key - Add a translation - "item.select.empty": "No items to show", + "item.select.empty": "Ei tietueita", // "item.select.table.author": "Author", - // TODO New key - Add a translation - "item.select.table.author": "Author", + "item.select.table.author": "Tekijä", // "item.select.table.collection": "Collection", - // TODO New key - Add a translation - "item.select.table.collection": "Collection", + "item.select.table.collection": "Kokoelma", // "item.select.table.title": "Title", - // TODO New key - Add a translation - "item.select.table.title": "Title", + "item.select.table.title": "Nimeke", // "item.version.history.empty": "There are no other versions for this item yet.", @@ -2657,92 +2280,71 @@ // "journal.listelement.badge": "Journal", - // TODO New key - Add a translation - "journal.listelement.badge": "Journal", + "journal.listelement.badge": "Kausijulkaisu", // "journal.page.description": "Description", - // TODO New key - Add a translation - "journal.page.description": "Description", + "journal.page.description": "Kuvaus", // "journal.page.editor": "Editor-in-Chief", - // TODO New key - Add a translation - "journal.page.editor": "Editor-in-Chief", + "journal.page.editor": "Päätoimittaja", // "journal.page.issn": "ISSN", - // TODO New key - Add a translation - "journal.page.issn": "ISSN", + "journal.page.issn": "ISSN-tunnus", // "journal.page.publisher": "Publisher", - // TODO New key - Add a translation - "journal.page.publisher": "Publisher", + "journal.page.publisher": "Julkaisija", // "journal.page.titleprefix": "Journal: ", - // TODO New key - Add a translation - "journal.page.titleprefix": "Journal: ", + "journal.page.titleprefix": "Kausijulkaisu: ", // "journal.search.results.head": "Journal Search Results", - // TODO New key - Add a translation - "journal.search.results.head": "Journal Search Results", + "journal.search.results.head": "Kausijulkaisuhaun tulokset", // "journal.search.title": "DSpace Angular :: Journal Search", - // TODO New key - Add a translation - "journal.search.title": "DSpace Angular :: Journal Search", + "journal.search.title": "DSpace Angular :: Kausijulkaisuhaku", // "journalissue.listelement.badge": "Journal Issue", - // TODO New key - Add a translation - "journalissue.listelement.badge": "Journal Issue", + "journalissue.listelement.badge": "Kausijulkaisun numero", // "journalissue.page.description": "Description", - // TODO New key - Add a translation - "journalissue.page.description": "Description", + "journalissue.page.description": "Kuvaus", // "journalissue.page.issuedate": "Issue Date", - // TODO New key - Add a translation - "journalissue.page.issuedate": "Issue Date", + "journalissue.page.issuedate": "Julkaisuaika", // "journalissue.page.journal-issn": "Journal ISSN", - // TODO New key - Add a translation - "journalissue.page.journal-issn": "Journal ISSN", + "journalissue.page.journal-issn": "Kausijulkaisun ISSN-tunnus", // "journalissue.page.journal-title": "Journal Title", - // TODO New key - Add a translation - "journalissue.page.journal-title": "Journal Title", + "journalissue.page.journal-title": "Kausijulkaisun nimi", // "journalissue.page.keyword": "Keywords", - // TODO New key - Add a translation - "journalissue.page.keyword": "Keywords", + "journalissue.page.keyword": "Asiasanat", // "journalissue.page.number": "Number", - // TODO New key - Add a translation - "journalissue.page.number": "Number", + "journalissue.page.number": "Numero", // "journalissue.page.titleprefix": "Journal Issue: ", - // TODO New key - Add a translation - "journalissue.page.titleprefix": "Journal Issue: ", + "journalissue.page.titleprefix": "Kausijulkaisun numero: ", // "journalvolume.listelement.badge": "Journal Volume", - // TODO New key - Add a translation - "journalvolume.listelement.badge": "Journal Volume", + "journalvolume.listelement.badge": "Kausijulkaisun vuosikerta", // "journalvolume.page.description": "Description", - // TODO New key - Add a translation - "journalvolume.page.description": "Description", + "journalvolume.page.description": "Kuvaus", // "journalvolume.page.issuedate": "Issue Date", - // TODO New key - Add a translation - "journalvolume.page.issuedate": "Issue Date", + "journalvolume.page.issuedate": "Julkaisuaika", // "journalvolume.page.titleprefix": "Journal Volume: ", - // TODO New key - Add a translation - "journalvolume.page.titleprefix": "Journal Volume: ", + "journalvolume.page.titleprefix": "Kausijulkaisun vuosikerta: ", // "journalvolume.page.volume": "Volume", - // TODO New key - Add a translation - "journalvolume.page.volume": "Volume", + "journalvolume.page.volume": "Vuosikerta", @@ -2755,106 +2357,84 @@ "loading.bitstreams": "Loading bitstreams...", // "loading.browse-by": "Loading items...", - // TODO New key - Add a translation - "loading.browse-by": "Loading items...", + "loading.browse-by": "Ladataan tietueita...", // "loading.browse-by-page": "Loading page...", - // TODO New key - Add a translation - "loading.browse-by-page": "Loading page...", + "loading.browse-by-page": "Ladataan sivua...", // "loading.collection": "Loading collection...", - // TODO New key - Add a translation - "loading.collection": "Loading collection...", + "loading.collection": "Ladataan kokoelmaa...", // "loading.collections": "Loading collections...", - // TODO New key - Add a translation - "loading.collections": "Loading collections...", + "loading.collections": "Ladataan kokoelmia...", // "loading.content-source": "Loading content source...", // TODO New key - Add a translation "loading.content-source": "Loading content source...", // "loading.community": "Loading community...", - // TODO New key - Add a translation - "loading.community": "Loading community...", + "loading.community": "Ladataan yhteisöä...", // "loading.default": "Loading...", - // TODO New key - Add a translation - "loading.default": "Loading...", + "loading.default": "Ladataan...", // "loading.item": "Loading item...", - // TODO New key - Add a translation - "loading.item": "Loading item...", + "loading.item": "Ladataan tietuetta...", // "loading.items": "Loading items...", - // TODO New key - Add a translation - "loading.items": "Loading items...", + "loading.items": "Ladataan tietueita...", // "loading.mydspace-results": "Loading items...", - // TODO New key - Add a translation - "loading.mydspace-results": "Loading items...", + "loading.mydspace-results": "Ladataan tietueita...", // "loading.objects": "Loading...", - // TODO New key - Add a translation - "loading.objects": "Loading...", + "loading.objects": "Ladataan...", // "loading.recent-submissions": "Loading recent submissions...", - // TODO New key - Add a translation - "loading.recent-submissions": "Loading recent submissions...", + "loading.recent-submissions": "Ladataan viimeksi lisättyjä...", // "loading.search-results": "Loading search results...", - // TODO New key - Add a translation - "loading.search-results": "Loading search results...", + "loading.search-results": "Ladataan hakutuloksia...", // "loading.sub-collections": "Loading sub-collections...", - // TODO New key - Add a translation - "loading.sub-collections": "Loading sub-collections...", + "loading.sub-collections": "Ladataan alakokoelmia...", // "loading.sub-communities": "Loading sub-communities...", - // TODO New key - Add a translation - "loading.sub-communities": "Loading sub-communities...", + "loading.sub-communities": "Ladataan alayhteisöjä...", // "loading.top-level-communities": "Loading top-level communities...", - // TODO New key - Add a translation - "loading.top-level-communities": "Loading top-level communities...", + "loading.top-level-communities": "Ladataan ylätason yhteisöjä...", // "login.form.email": "Email address", - // TODO New key - Add a translation - "login.form.email": "Email address", + "login.form.email": "Sähköpostiosoite", // "login.form.forgot-password": "Have you forgotten your password?", - // TODO New key - Add a translation - "login.form.forgot-password": "Have you forgotten your password?", + "login.form.forgot-password": "Unohditko salasanasi?", // "login.form.header": "Please log in to DSpace", - // TODO New key - Add a translation - "login.form.header": "Please log in to DSpace", + "login.form.header": "Kirjaudu sisään", // "login.form.new-user": "New user? Click here to register.", - // TODO New key - Add a translation - "login.form.new-user": "New user? Click here to register.", + "login.form.new-user": "Uusi käyttäjä? Rekisteröidy tästä.", // "login.form.or-divider": "or", // TODO New key - Add a translation "login.form.or-divider": "or", // "login.form.password": "Password", - // TODO New key - Add a translation - "login.form.password": "Password", + "login.form.password": "Salasana", // "login.form.shibboleth": "Log in with Shibboleth", // TODO New key - Add a translation "login.form.shibboleth": "Log in with Shibboleth", // "login.form.submit": "Log in", - // TODO New key - Add a translation - "login.form.submit": "Log in", + "login.form.submit": "Kirjaudu sisään", // "login.title": "Login", - // TODO New key - Add a translation - "login.title": "Login", + "login.title": "Sisäänkirjautuminen", // "login.breadcrumbs": "Login", // TODO New key - Add a translation @@ -2863,44 +2443,35 @@ // "logout.form.header": "Log out from DSpace", - // TODO New key - Add a translation - "logout.form.header": "Log out from DSpace", + "logout.form.header": "Kirjaudu ulos", // "logout.form.submit": "Log out", - // TODO New key - Add a translation - "logout.form.submit": "Log out", + "logout.form.submit": "Kirjaudu ulos", // "logout.title": "Logout", - // TODO New key - Add a translation - "logout.title": "Logout", + "logout.title": "Uloskirjautuminen", // "menu.header.admin": "Admin", - // TODO New key - Add a translation - "menu.header.admin": "Admin", + "menu.header.admin": "Ylläpitäjä", // "menu.header.image.logo": "Repository logo", - // TODO New key - Add a translation - "menu.header.image.logo": "Repository logo", + "menu.header.image.logo": "Arkiston logo", // "menu.section.access_control": "Access Control", - // TODO New key - Add a translation - "menu.section.access_control": "Access Control", + "menu.section.access_control": "Pääsyoikeudet", // "menu.section.access_control_authorizations": "Authorizations", - // TODO New key - Add a translation - "menu.section.access_control_authorizations": "Authorizations", + "menu.section.access_control_authorizations": "Käyttöoikeudet", // "menu.section.access_control_groups": "Groups", - // TODO New key - Add a translation - "menu.section.access_control_groups": "Groups", + "menu.section.access_control_groups": "Ryhmät", // "menu.section.access_control_people": "People", - // TODO New key - Add a translation - "menu.section.access_control_people": "People", + "menu.section.access_control_people": "Käyttäjät", @@ -2911,546 +2482,420 @@ // "menu.section.browse_community": "This Community", - // TODO New key - Add a translation - "menu.section.browse_community": "This Community", + "menu.section.browse_community": "Tämä yhteisö", // "menu.section.browse_community_by_author": "By Author", - // TODO New key - Add a translation - "menu.section.browse_community_by_author": "By Author", + "menu.section.browse_community_by_author": "Tekijän mukaan", // "menu.section.browse_community_by_issue_date": "By Issue Date", - // TODO New key - Add a translation - "menu.section.browse_community_by_issue_date": "By Issue Date", + "menu.section.browse_community_by_issue_date": "Julkaisuajankohdan mukaan", // "menu.section.browse_community_by_title": "By Title", - // TODO New key - Add a translation - "menu.section.browse_community_by_title": "By Title", + "menu.section.browse_community_by_title": "Nimekkeen mukaan", // "menu.section.browse_global": "All of DSpace", - // TODO New key - Add a translation - "menu.section.browse_global": "All of DSpace", + "menu.section.browse_global": "Koko julkaisuarkisto", // "menu.section.browse_global_by_author": "By Author", - // TODO New key - Add a translation - "menu.section.browse_global_by_author": "By Author", + "menu.section.browse_global_by_author": "Tekijän mukaan", // "menu.section.browse_global_by_dateissued": "By Issue Date", - // TODO New key - Add a translation - "menu.section.browse_global_by_dateissued": "By Issue Date", + "menu.section.browse_global_by_dateissued": "Julkaisuajankohdan mukaan", // "menu.section.browse_global_by_subject": "By Subject", - // TODO New key - Add a translation - "menu.section.browse_global_by_subject": "By Subject", + "menu.section.browse_global_by_subject": "Avainsanan mukaan", // "menu.section.browse_global_by_title": "By Title", - // TODO New key - Add a translation - "menu.section.browse_global_by_title": "By Title", + "menu.section.browse_global_by_title": "Nimekkeen mukaan", // "menu.section.browse_global_communities_and_collections": "Communities & Collections", - // TODO New key - Add a translation - "menu.section.browse_global_communities_and_collections": "Communities & Collections", + "menu.section.browse_global_communities_and_collections": "Yhteisöt & kokoelmat", // "menu.section.control_panel": "Control Panel", - // TODO New key - Add a translation - "menu.section.control_panel": "Control Panel", + "menu.section.control_panel": "Hallintapaneeli", // "menu.section.curation_task": "Curation Task", - // TODO New key - Add a translation - "menu.section.curation_task": "Curation Task", + "menu.section.curation_task": "Kuratointitehtävä", // "menu.section.edit": "Edit", - // TODO New key - Add a translation - "menu.section.edit": "Edit", + "menu.section.edit": "Muokkaa", // "menu.section.edit_collection": "Collection", - // TODO New key - Add a translation - "menu.section.edit_collection": "Collection", + "menu.section.edit_collection": "Kokoelma", // "menu.section.edit_community": "Community", - // TODO New key - Add a translation - "menu.section.edit_community": "Community", + "menu.section.edit_community": "Yhteisö", // "menu.section.edit_item": "Item", - // TODO New key - Add a translation - "menu.section.edit_item": "Item", + "menu.section.edit_item": "Tietue", // "menu.section.export": "Export", - // TODO New key - Add a translation - "menu.section.export": "Export", + "menu.section.export": "Eksportoi", // "menu.section.export_collection": "Collection", - // TODO New key - Add a translation - "menu.section.export_collection": "Collection", + "menu.section.export_collection": "Kokoelma", // "menu.section.export_community": "Community", - // TODO New key - Add a translation - "menu.section.export_community": "Community", + "menu.section.export_community": "Yhteisö", // "menu.section.export_item": "Item", - // TODO New key - Add a translation - "menu.section.export_item": "Item", + "menu.section.export_item": "Tietue", // "menu.section.export_metadata": "Metadata", - // TODO New key - Add a translation "menu.section.export_metadata": "Metadata", // "menu.section.icon.access_control": "Access Control menu section", - // TODO New key - Add a translation - "menu.section.icon.access_control": "Access Control menu section", + "menu.section.icon.access_control": "Pääsyoikeudet", // "menu.section.icon.admin_search": "Admin search menu section", // TODO New key - Add a translation "menu.section.icon.admin_search": "Admin search menu section", // "menu.section.icon.control_panel": "Control Panel menu section", - // TODO New key - Add a translation - "menu.section.icon.control_panel": "Control Panel menu section", + "menu.section.icon.control_panel": "Hallintapaneeli", // "menu.section.icon.curation_task": "Curation Task menu section", - // TODO New key - Add a translation - "menu.section.icon.curation_task": "Curation Task menu section", + "menu.section.icon.curation_task": "Kuratointi", // "menu.section.icon.edit": "Edit menu section", - // TODO New key - Add a translation - "menu.section.icon.edit": "Edit menu section", + "menu.section.icon.edit": "Muokkaus", // "menu.section.icon.export": "Export menu section", - // TODO New key - Add a translation - "menu.section.icon.export": "Export menu section", + "menu.section.icon.export": "Eksportointi", // "menu.section.icon.find": "Find menu section", - // TODO New key - Add a translation - "menu.section.icon.find": "Find menu section", + "menu.section.icon.find": "Haku", // "menu.section.icon.import": "Import menu section", - // TODO New key - Add a translation - "menu.section.icon.import": "Import menu section", + "menu.section.icon.import": "Importointi", // "menu.section.icon.new": "New menu section", - // TODO New key - Add a translation - "menu.section.icon.new": "New menu section", + "menu.section.icon.new": "Uusi", // "menu.section.icon.pin": "Pin sidebar", - // TODO New key - Add a translation - "menu.section.icon.pin": "Pin sidebar", + "menu.section.icon.pin": "Kiinnitä sivupalkki", // "menu.section.icon.registries": "Registries menu section", - // TODO New key - Add a translation - "menu.section.icon.registries": "Registries menu section", + "menu.section.icon.registries": "Rekisterit", // "menu.section.icon.statistics_task": "Statistics Task menu section", - // TODO New key - Add a translation - "menu.section.icon.statistics_task": "Statistics Task menu section", + "menu.section.icon.statistics_task": "Tilastot", // "menu.section.icon.unpin": "Unpin sidebar", - // TODO New key - Add a translation - "menu.section.icon.unpin": "Unpin sidebar", + "menu.section.icon.unpin": "Vapauta sivupalkki", // "menu.section.import": "Import", - // TODO New key - Add a translation - "menu.section.import": "Import", + "menu.section.import": "Importoi", // "menu.section.import_batch": "Batch Import (ZIP)", - // TODO New key - Add a translation - "menu.section.import_batch": "Batch Import (ZIP)", + "menu.section.import_batch": "Importoi useamman tiedoston erä (ZIP)", // "menu.section.import_metadata": "Metadata", - // TODO New key - Add a translation "menu.section.import_metadata": "Metadata", // "menu.section.new": "New", - // TODO New key - Add a translation - "menu.section.new": "New", + "menu.section.new": "Uusi", // "menu.section.new_collection": "Collection", - // TODO New key - Add a translation - "menu.section.new_collection": "Collection", + "menu.section.new_collection": "Kokoelma", // "menu.section.new_community": "Community", - // TODO New key - Add a translation - "menu.section.new_community": "Community", + "menu.section.new_community": "Yhteisö", // "menu.section.new_item": "Item", - // TODO New key - Add a translation - "menu.section.new_item": "Item", + "menu.section.new_item": "Tietue", // "menu.section.new_item_version": "Item Version", - // TODO New key - Add a translation - "menu.section.new_item_version": "Item Version", + "menu.section.new_item_version": "Tietueen versio", // "menu.section.pin": "Pin sidebar", - // TODO New key - Add a translation - "menu.section.pin": "Pin sidebar", + "menu.section.pin": "Kiinnitä sivupalkki", // "menu.section.unpin": "Unpin sidebar", - // TODO New key - Add a translation - "menu.section.unpin": "Unpin sidebar", + "menu.section.unpin": "Vapauta sivupalkki", // "menu.section.registries": "Registries", - // TODO New key - Add a translation - "menu.section.registries": "Registries", + "menu.section.registries": "Rekisterit", // "menu.section.registries_format": "Format", - // TODO New key - Add a translation - "menu.section.registries_format": "Format", + "menu.section.registries_format": "Formaatti", // "menu.section.registries_metadata": "Metadata", - // TODO New key - Add a translation "menu.section.registries_metadata": "Metadata", // "menu.section.statistics": "Statistics", - // TODO New key - Add a translation - "menu.section.statistics": "Statistics", + "menu.section.statistics": "Tilastot", // "menu.section.statistics_task": "Statistics Task", - // TODO New key - Add a translation - "menu.section.statistics_task": "Statistics Task", + "menu.section.statistics_task": "Tilastointitehtävä", // "menu.section.toggle.access_control": "Toggle Access Control section", - // TODO New key - Add a translation - "menu.section.toggle.access_control": "Toggle Access Control section", + "menu.section.toggle.access_control": "Vaihda Pääsyoikeudet-osion tilaa", // "menu.section.toggle.control_panel": "Toggle Control Panel section", - // TODO New key - Add a translation - "menu.section.toggle.control_panel": "Toggle Control Panel section", + "menu.section.toggle.control_panel": "Vaihda Hallintapaneeli-osion tilaa", // "menu.section.toggle.curation_task": "Toggle Curation Task section", - // TODO New key - Add a translation - "menu.section.toggle.curation_task": "Toggle Curation Task section", + "menu.section.toggle.curation_task": "Vaihda Kuratointitehtävä-osion tilaa", // "menu.section.toggle.edit": "Toggle Edit section", - // TODO New key - Add a translation - "menu.section.toggle.edit": "Toggle Edit section", + "menu.section.toggle.edit": "Vaihda Muokkaus-osion tilaa", // "menu.section.toggle.export": "Toggle Export section", - // TODO New key - Add a translation - "menu.section.toggle.export": "Toggle Export section", + "menu.section.toggle.export": "Vaihda Eksportointi-osion tilaa", // "menu.section.toggle.find": "Toggle Find section", - // TODO New key - Add a translation - "menu.section.toggle.find": "Toggle Find section", + "menu.section.toggle.find": "Vaihda Haku-osion tilaa", // "menu.section.toggle.import": "Toggle Import section", - // TODO New key - Add a translation - "menu.section.toggle.import": "Toggle Import section", + "menu.section.toggle.import": "Vaihda Importointi-osion tilaa", // "menu.section.toggle.new": "Toggle New section", - // TODO New key - Add a translation - "menu.section.toggle.new": "Toggle New section", + "menu.section.toggle.new": "Vaihda Uusi-osion tilaa", // "menu.section.toggle.registries": "Toggle Registries section", - // TODO New key - Add a translation - "menu.section.toggle.registries": "Toggle Registries section", + "menu.section.toggle.registries": "Vaihda Rekisterit-osion tilaa", // "menu.section.toggle.statistics_task": "Toggle Statistics Task section", - // TODO New key - Add a translation - "menu.section.toggle.statistics_task": "Toggle Statistics Task section", + "menu.section.toggle.statistics_task": "Vaihda Tilastointitehtävä-osion tilaa", // "mydspace.description": "", - // TODO New key - Add a translation "mydspace.description": "", // "mydspace.general.text-here": "HERE", - // TODO New key - Add a translation - "mydspace.general.text-here": "HERE", + "mydspace.general.text-here": "TÄSSÄ", // "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", - // TODO New key - Add a translation - "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", + "mydspace.messages.controller-help": "Valitse tämä, jos haluat lähettää viestin tietueen julkaisijalle.", // "mydspace.messages.description-placeholder": "Insert your message here...", - // TODO New key - Add a translation - "mydspace.messages.description-placeholder": "Insert your message here...", + "mydspace.messages.description-placeholder": "Kirjoita viestisi tähän...", // "mydspace.messages.hide-msg": "Hide message", - // TODO New key - Add a translation - "mydspace.messages.hide-msg": "Hide message", + "mydspace.messages.hide-msg": "Piilota viesti", // "mydspace.messages.mark-as-read": "Mark as read", - // TODO New key - Add a translation - "mydspace.messages.mark-as-read": "Mark as read", + "mydspace.messages.mark-as-read": "Merkitse luetuksi", // "mydspace.messages.mark-as-unread": "Mark as unread", - // TODO New key - Add a translation - "mydspace.messages.mark-as-unread": "Mark as unread", + "mydspace.messages.mark-as-unread": "Merkitse lukemattomaksi", // "mydspace.messages.no-content": "No content.", - // TODO New key - Add a translation - "mydspace.messages.no-content": "No content.", + "mydspace.messages.no-content": "Ei sisältöä.", // "mydspace.messages.no-messages": "No messages yet.", - // TODO New key - Add a translation - "mydspace.messages.no-messages": "No messages yet.", + "mydspace.messages.no-messages": "Ei viestejä.", // "mydspace.messages.send-btn": "Send", - // TODO New key - Add a translation - "mydspace.messages.send-btn": "Send", + "mydspace.messages.send-btn": "Lähetä", // "mydspace.messages.show-msg": "Show message", - // TODO New key - Add a translation - "mydspace.messages.show-msg": "Show message", + "mydspace.messages.show-msg": "Näytä viesti", // "mydspace.messages.subject-placeholder": "Subject...", - // TODO New key - Add a translation - "mydspace.messages.subject-placeholder": "Subject...", + "mydspace.messages.subject-placeholder": "Asiasana...", // "mydspace.messages.submitter-help": "Select this option to send a message to controller.", - // TODO New key - Add a translation - "mydspace.messages.submitter-help": "Select this option to send a message to controller.", + "mydspace.messages.submitter-help": "Valitse tämä, jos haluat lähettää viestin tarkastajalle.", // "mydspace.messages.title": "Messages", - // TODO New key - Add a translation - "mydspace.messages.title": "Messages", + "mydspace.messages.title": "Viestit", // "mydspace.messages.to": "To", - // TODO New key - Add a translation - "mydspace.messages.to": "To", + "mydspace.messages.to": "Vastaanottaja", // "mydspace.new-submission": "New submission", - // TODO New key - Add a translation - "mydspace.new-submission": "New submission", + "mydspace.new-submission": "Uusi julkaisu", // "mydspace.results.head": "Your submissions", - // TODO New key - Add a translation - "mydspace.results.head": "Your submissions", + "mydspace.results.head": "Julkaisusi", // "mydspace.results.no-abstract": "No Abstract", - // TODO New key - Add a translation - "mydspace.results.no-abstract": "No Abstract", + "mydspace.results.no-abstract": "Ei tiivistelmää", // "mydspace.results.no-authors": "No Authors", - // TODO New key - Add a translation - "mydspace.results.no-authors": "No Authors", + "mydspace.results.no-authors": "Ei tekijöitä", // "mydspace.results.no-collections": "No Collections", - // TODO New key - Add a translation - "mydspace.results.no-collections": "No Collections", + "mydspace.results.no-collections": "Ei kokoelmia", // "mydspace.results.no-date": "No Date", - // TODO New key - Add a translation - "mydspace.results.no-date": "No Date", + "mydspace.results.no-date": "Ei päivämäärää", // "mydspace.results.no-files": "No Files", - // TODO New key - Add a translation - "mydspace.results.no-files": "No Files", + "mydspace.results.no-files": "Ei tiedostoja", // "mydspace.results.no-results": "There were no items to show", - // TODO New key - Add a translation - "mydspace.results.no-results": "There were no items to show", + "mydspace.results.no-results": "Ei tietueita", // "mydspace.results.no-title": "No title", - // TODO New key - Add a translation - "mydspace.results.no-title": "No title", + "mydspace.results.no-title": "Ei nimikettä", // "mydspace.results.no-uri": "No Uri", - // TODO New key - Add a translation - "mydspace.results.no-uri": "No Uri", + "mydspace.results.no-uri": "Ei URL-osoitetta", // "mydspace.show.workflow": "All tasks", - // TODO New key - Add a translation - "mydspace.show.workflow": "All tasks", + "mydspace.show.workflow": "Kaikki tehtävät", // "mydspace.show.workspace": "Your Submissions", - // TODO New key - Add a translation - "mydspace.show.workspace": "Your Submissions", + "mydspace.show.workspace": "Julkaisusi", // "mydspace.status.archived": "Archived", - // TODO New key - Add a translation - "mydspace.status.archived": "Archived", + "mydspace.status.archived": "Arkistoitu", // "mydspace.status.validation": "Validation", - // TODO New key - Add a translation - "mydspace.status.validation": "Validation", + "mydspace.status.validation": "Tarkastaminen", // "mydspace.status.waiting-for-controller": "Waiting for controller", - // TODO New key - Add a translation - "mydspace.status.waiting-for-controller": "Waiting for controller", + "mydspace.status.waiting-for-controller": "Odotetaan tarkastajaa", // "mydspace.status.workflow": "Workflow", - // TODO New key - Add a translation - "mydspace.status.workflow": "Workflow", + "mydspace.status.workflow": "Työnkulku", // "mydspace.status.workspace": "Workspace", - // TODO New key - Add a translation - "mydspace.status.workspace": "Workspace", + "mydspace.status.workspace": "Työtila", // "mydspace.title": "MyDSpace", - // TODO New key - Add a translation - "mydspace.title": "MyDSpace", + "mydspace.title": "Omat tiedot", // "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", - // TODO New key - Add a translation - "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", + "mydspace.upload.upload-failed": "Virhe uutta työtilaa luotaessa. Tarkista ladattava sisältö ennen kuin yrität uudelleen.", // "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", - // TODO New key - Add a translation - "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", + "mydspace.upload.upload-multiple-successful": "{{qty}} uutta työtilaa luotu.", // "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", - // TODO New key - Add a translation - "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", + "mydspace.upload.upload-successful": "Uusi työtila luotu. Napauta tästä muokataksesi sitä.", // "mydspace.view-btn": "View", - // TODO New key - Add a translation - "mydspace.view-btn": "View", + "mydspace.view-btn": "Näytä", // "nav.browse.header": "All of DSpace", - // TODO New key - Add a translation - "nav.browse.header": "All of DSpace", + "nav.browse.header": "Koko julkaisuarkisto", // "nav.community-browse.header": "By Community", - // TODO New key - Add a translation - "nav.community-browse.header": "By Community", + "nav.community-browse.header": "Yhteisön mukaan", // "nav.language": "Language switch", - // TODO New key - Add a translation - "nav.language": "Language switch", + "nav.language": "Kielivalinta", // "nav.login": "Log In", - // TODO New key - Add a translation - "nav.login": "Log In", + "nav.login": "Kirjaudu sisään", // "nav.logout": "Log Out", - // TODO New key - Add a translation - "nav.logout": "Log Out", + "nav.logout": "Kirjaudu ulos", // "nav.mydspace": "MyDSpace", - // TODO New key - Add a translation - "nav.mydspace": "MyDSpace", + "nav.mydspace": "Omat tiedot", // "nav.profile": "Profile", // TODO New key - Add a translation "nav.profile": "Profile", // "nav.search": "Search", - // TODO New key - Add a translation - "nav.search": "Search", + "nav.search": "Hae", // "nav.statistics.header": "Statistics", - // TODO New key - Add a translation - "nav.statistics.header": "Statistics", + "nav.statistics.header": "Tilastot", // "orgunit.listelement.badge": "Organizational Unit", - // TODO New key - Add a translation - "orgunit.listelement.badge": "Organizational Unit", + "orgunit.listelement.badge": "Organisaatioyksikkö", // "orgunit.page.city": "City", - // TODO New key - Add a translation - "orgunit.page.city": "City", + "orgunit.page.city": "Kaupunki", // "orgunit.page.country": "Country", - // TODO New key - Add a translation - "orgunit.page.country": "Country", + "orgunit.page.country": "Maa", // "orgunit.page.dateestablished": "Date established", - // TODO New key - Add a translation - "orgunit.page.dateestablished": "Date established", + "orgunit.page.dateestablished": "Perustamispäivämäärä", // "orgunit.page.description": "Description", - // TODO New key - Add a translation - "orgunit.page.description": "Description", + "orgunit.page.description": "Kuvaus", // "orgunit.page.id": "ID", - // TODO New key - Add a translation - "orgunit.page.id": "ID", + "orgunit.page.id": "ID-tunnus", // "orgunit.page.titleprefix": "Organizational Unit: ", - // TODO New key - Add a translation - "orgunit.page.titleprefix": "Organizational Unit: ", + "orgunit.page.titleprefix": "Organisaatioyksikkö: ", // "pagination.results-per-page": "Results Per Page", - // TODO New key - Add a translation - "pagination.results-per-page": "Results Per Page", + "pagination.results-per-page": "Tuloksia sivulla", // "pagination.showing.detail": "{{ range }} of {{ total }}", - // TODO New key - Add a translation - "pagination.showing.detail": "{{ range }} of {{ total }}", + "pagination.showing.detail": "{{ range }} / {{ total }}", // "pagination.showing.label": "Now showing ", - // TODO New key - Add a translation - "pagination.showing.label": "Now showing ", + "pagination.showing.label": "Näytetään ", // "pagination.sort-direction": "Sort Options", - // TODO New key - Add a translation - "pagination.sort-direction": "Sort Options", + "pagination.sort-direction": "Lajitteluvalinnat", // "person.listelement.badge": "Person", - // TODO New key - Add a translation - "person.listelement.badge": "Person", + "person.listelement.badge": "Käyttäjä", // "person.page.birthdate": "Birth Date", - // TODO New key - Add a translation - "person.page.birthdate": "Birth Date", + "person.page.birthdate": "Syntymäaika", // "person.page.email": "Email Address", - // TODO New key - Add a translation - "person.page.email": "Email Address", + "person.page.email": "Sähköpostiosoite", // "person.page.firstname": "First Name", - // TODO New key - Add a translation - "person.page.firstname": "First Name", + "person.page.firstname": "Etunimi", // "person.page.jobtitle": "Job Title", - // TODO New key - Add a translation - "person.page.jobtitle": "Job Title", + "person.page.jobtitle": "Tehtävänimike", // "person.page.lastname": "Last Name", - // TODO New key - Add a translation - "person.page.lastname": "Last Name", + "person.page.lastname": "Sukunimi", // "person.page.link.full": "Show all metadata", - // TODO New key - Add a translation - "person.page.link.full": "Show all metadata", + "person.page.link.full": "Näytä kaikki metadata", // "person.page.orcid": "ORCID", - // TODO New key - Add a translation - "person.page.orcid": "ORCID", + "person.page.orcid": "ORCID-tunniste", // "person.page.staffid": "Staff ID", - // TODO New key - Add a translation - "person.page.staffid": "Staff ID", + "person.page.staffid": "Henkilökunnan ID-tunnus", // "person.page.titleprefix": "Person: ", - // TODO New key - Add a translation - "person.page.titleprefix": "Person: ", + "person.page.titleprefix": "Käyttäjä: ", // "person.search.results.head": "Person Search Results", - // TODO New key - Add a translation - "person.search.results.head": "Person Search Results", + "person.search.results.head": "Käyttäjähaun tulokset", // "person.search.title": "DSpace Angular :: Person Search", - // TODO New key - Add a translation - "person.search.title": "DSpace Angular :: Person Search", + "person.search.title": "DSpace Angular :: Käyttäjähaku", @@ -3569,40 +3014,31 @@ // "project.listelement.badge": "Research Project", - // TODO New key - Add a translation - "project.listelement.badge": "Research Project", + "project.listelement.badge": "Tutkimusprojekti", // "project.page.contributor": "Contributors", - // TODO New key - Add a translation - "project.page.contributor": "Contributors", + "project.page.contributor": "Muut tekijät", // "project.page.description": "Description", - // TODO New key - Add a translation - "project.page.description": "Description", + "project.page.description": "Kuvaus", // "project.page.expectedcompletion": "Expected Completion", - // TODO New key - Add a translation - "project.page.expectedcompletion": "Expected Completion", + "project.page.expectedcompletion": "Todennäköinen päättyminen", // "project.page.funder": "Funders", - // TODO New key - Add a translation - "project.page.funder": "Funders", + "project.page.funder": "Rahoittajat", // "project.page.id": "ID", - // TODO New key - Add a translation - "project.page.id": "ID", + "project.page.id": "ID-tunnus", // "project.page.keyword": "Keywords", - // TODO New key - Add a translation - "project.page.keyword": "Keywords", + "project.page.keyword": "Asiasanat", // "project.page.status": "Status", - // TODO New key - Add a translation - "project.page.status": "Status", + "project.page.status": "Tila", // "project.page.titleprefix": "Research Project: ", - // TODO New key - Add a translation - "project.page.titleprefix": "Research Project: ", + "project.page.titleprefix": "Tutkimusprojekti: ", // "project.search.results.head": "Project Search Results", // TODO New key - Add a translation @@ -3611,90 +3047,69 @@ // "publication.listelement.badge": "Publication", - // TODO New key - Add a translation - "publication.listelement.badge": "Publication", + "publication.listelement.badge": "Julkaisu", // "publication.page.description": "Description", - // TODO New key - Add a translation - "publication.page.description": "Description", + "publication.page.description": "Kuvaus", // "publication.page.journal-issn": "Journal ISSN", - // TODO New key - Add a translation - "publication.page.journal-issn": "Journal ISSN", + "publication.page.journal-issn": "Kausijulkaisun ISSN-tunnus", // "publication.page.journal-title": "Journal Title", - // TODO New key - Add a translation - "publication.page.journal-title": "Journal Title", + "publication.page.journal-title": "Kausijulkaisun nimi", // "publication.page.publisher": "Publisher", - // TODO New key - Add a translation - "publication.page.publisher": "Publisher", + "publication.page.publisher": "Julkaisija", // "publication.page.titleprefix": "Publication: ", - // TODO New key - Add a translation - "publication.page.titleprefix": "Publication: ", + "publication.page.titleprefix": "Julkaisu: ", // "publication.page.volume-title": "Volume Title", - // TODO New key - Add a translation - "publication.page.volume-title": "Volume Title", + "publication.page.volume-title": "Vuosikerran nimi", // "publication.search.results.head": "Publication Search Results", - // TODO New key - Add a translation - "publication.search.results.head": "Publication Search Results", + "publication.search.results.head": "Aineistohaun tulokset", // "publication.search.title": "DSpace Angular :: Publication Search", - // TODO New key - Add a translation - "publication.search.title": "DSpace Angular :: Publication Search", + "publication.search.title": "DSpace Angular :: Aineistohaku", // "relationships.isAuthorOf": "Authors", - // TODO New key - Add a translation - "relationships.isAuthorOf": "Authors", + "relationships.isAuthorOf": "Tekijät", // "relationships.isIssueOf": "Journal Issues", - // TODO New key - Add a translation - "relationships.isIssueOf": "Journal Issues", + "relationships.isIssueOf": "Kausijulkaisun numerot", // "relationships.isJournalIssueOf": "Journal Issue", - // TODO New key - Add a translation - "relationships.isJournalIssueOf": "Journal Issue", + "relationships.isJournalIssueOf": "Kausijulkaisun numero", // "relationships.isJournalOf": "Journals", - // TODO New key - Add a translation - "relationships.isJournalOf": "Journals", + "relationships.isJournalOf": "Kausijulkaisut", // "relationships.isOrgUnitOf": "Organizational Units", - // TODO New key - Add a translation - "relationships.isOrgUnitOf": "Organizational Units", + "relationships.isOrgUnitOf": "Organisaatioyksiköt", // "relationships.isPersonOf": "Authors", - // TODO New key - Add a translation - "relationships.isPersonOf": "Authors", + "relationships.isPersonOf": "Tekijät", // "relationships.isProjectOf": "Research Projects", - // TODO New key - Add a translation - "relationships.isProjectOf": "Research Projects", + "relationships.isProjectOf": "Tutkimusprojektit", // "relationships.isPublicationOf": "Publications", - // TODO New key - Add a translation - "relationships.isPublicationOf": "Publications", + "relationships.isPublicationOf": "Julkaisut", // "relationships.isPublicationOfJournalIssue": "Articles", - // TODO New key - Add a translation - "relationships.isPublicationOfJournalIssue": "Articles", + "relationships.isPublicationOfJournalIssue": "Artikkelit", // "relationships.isSingleJournalOf": "Journal", - // TODO New key - Add a translation - "relationships.isSingleJournalOf": "Journal", + "relationships.isSingleJournalOf": "Kausijulkaisu", // "relationships.isSingleVolumeOf": "Journal Volume", - // TODO New key - Add a translation - "relationships.isSingleVolumeOf": "Journal Volume", + "relationships.isSingleVolumeOf": "Kausijulkaisun vuosikerta", // "relationships.isVolumeOf": "Journal Volumes", - // TODO New key - Add a translation - "relationships.isVolumeOf": "Journal Volumes", + "relationships.isVolumeOf": "Kausijulkaisun vuosikerrat", // "relationships.isContributorOf": "Contributors", // TODO New key - Add a translation @@ -3703,16 +3118,13 @@ // "search.description": "", - // TODO New key - Add a translation "search.description": "", // "search.switch-configuration.title": "Show", - // TODO New key - Add a translation - "search.switch-configuration.title": "Show", + "search.switch-configuration.title": "Näytä", // "search.title": "DSpace Angular :: Search", - // TODO New key - Add a translation - "search.title": "DSpace Angular :: Search", + "search.title": "DSpace Angular :: Hae", // "search.breadcrumbs": "Search", // TODO New key - Add a translation @@ -3720,48 +3132,38 @@ // "search.filters.applied.f.author": "Author", - // TODO New key - Add a translation - "search.filters.applied.f.author": "Author", + "search.filters.applied.f.author": "Tekijä", // "search.filters.applied.f.dateIssued.max": "End date", - // TODO New key - Add a translation - "search.filters.applied.f.dateIssued.max": "End date", + "search.filters.applied.f.dateIssued.max": "Loppupäivämäärä", // "search.filters.applied.f.dateIssued.min": "Start date", - // TODO New key - Add a translation - "search.filters.applied.f.dateIssued.min": "Start date", + "search.filters.applied.f.dateIssued.min": "Alkupäivämäärä", // "search.filters.applied.f.dateSubmitted": "Date submitted", - // TODO New key - Add a translation - "search.filters.applied.f.dateSubmitted": "Date submitted", + "search.filters.applied.f.dateSubmitted": "Tallennusajankohta", // "search.filters.applied.f.discoverable": "Private", // TODO New key - Add a translation "search.filters.applied.f.discoverable": "Private", // "search.filters.applied.f.entityType": "Item Type", - // TODO New key - Add a translation - "search.filters.applied.f.entityType": "Item Type", + "search.filters.applied.f.entityType": "Tietueen tyyppi", // "search.filters.applied.f.has_content_in_original_bundle": "Has files", - // TODO New key - Add a translation - "search.filters.applied.f.has_content_in_original_bundle": "Has files", + "search.filters.applied.f.has_content_in_original_bundle": "Tiedostot:", // "search.filters.applied.f.itemtype": "Type", - // TODO New key - Add a translation - "search.filters.applied.f.itemtype": "Type", + "search.filters.applied.f.itemtype": "Tyyppi", // "search.filters.applied.f.namedresourcetype": "Status", - // TODO New key - Add a translation - "search.filters.applied.f.namedresourcetype": "Status", + "search.filters.applied.f.namedresourcetype": "Tila", // "search.filters.applied.f.subject": "Subject", - // TODO New key - Add a translation - "search.filters.applied.f.subject": "Subject", + "search.filters.applied.f.subject": "Asiasana", // "search.filters.applied.f.submitter": "Submitter", - // TODO New key - Add a translation - "search.filters.applied.f.submitter": "Submitter", + "search.filters.applied.f.submitter": "Julkaisija", // "search.filters.applied.f.jobTitle": "Job Title", // TODO New key - Add a translation @@ -3782,72 +3184,55 @@ // "search.filters.filter.author.head": "Author", - // TODO New key - Add a translation - "search.filters.filter.author.head": "Author", + "search.filters.filter.author.head": "Tekijä", // "search.filters.filter.author.placeholder": "Author name", - // TODO New key - Add a translation - "search.filters.filter.author.placeholder": "Author name", + "search.filters.filter.author.placeholder": "Tekijän nimi", // "search.filters.filter.birthDate.head": "Birth Date", - // TODO New key - Add a translation - "search.filters.filter.birthDate.head": "Birth Date", + "search.filters.filter.birthDate.head": "Syntymäaika", // "search.filters.filter.birthDate.placeholder": "Birth Date", - // TODO New key - Add a translation - "search.filters.filter.birthDate.placeholder": "Birth Date", + "search.filters.filter.birthDate.placeholder": "Syntymäaika", // "search.filters.filter.creativeDatePublished.head": "Date Published", - // TODO New key - Add a translation - "search.filters.filter.creativeDatePublished.head": "Date Published", + "search.filters.filter.creativeDatePublished.head": "Julkaisuajankohta", // "search.filters.filter.creativeDatePublished.placeholder": "Date Published", - // TODO New key - Add a translation - "search.filters.filter.creativeDatePublished.placeholder": "Date Published", + "search.filters.filter.creativeDatePublished.placeholder": "Julkaisuajankohta", // "search.filters.filter.creativeWorkEditor.head": "Editor", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkEditor.head": "Editor", + "search.filters.filter.creativeWorkEditor.head": "Toimittaja", // "search.filters.filter.creativeWorkEditor.placeholder": "Editor", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkEditor.placeholder": "Editor", + "search.filters.filter.creativeWorkEditor.placeholder": "Toimittaja", // "search.filters.filter.creativeWorkKeywords.head": "Subject", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkKeywords.head": "Subject", + "search.filters.filter.creativeWorkKeywords.head": "Asiasana", // "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", + "search.filters.filter.creativeWorkKeywords.placeholder": "Asiasana", // "search.filters.filter.creativeWorkPublisher.head": "Publisher", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkPublisher.head": "Publisher", + "search.filters.filter.creativeWorkPublisher.head": "Julkaisija", // "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", + "search.filters.filter.creativeWorkPublisher.placeholder": "Julkaisija", // "search.filters.filter.dateIssued.head": "Date", - // TODO New key - Add a translation - "search.filters.filter.dateIssued.head": "Date", + "search.filters.filter.dateIssued.head": "Päivämäärä", // "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", - // TODO New key - Add a translation - "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", + "search.filters.filter.dateIssued.max.placeholder": "Alkupäivämäärä", // "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", - // TODO New key - Add a translation - "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", + "search.filters.filter.dateIssued.min.placeholder": "Loppupäivämäärä", // "search.filters.filter.dateSubmitted.head": "Date submitted", - // TODO New key - Add a translation - "search.filters.filter.dateSubmitted.head": "Date submitted", + "search.filters.filter.dateSubmitted.head": "Tallennusajankohta", // "search.filters.filter.dateSubmitted.placeholder": "Date submitted", - // TODO New key - Add a translation - "search.filters.filter.dateSubmitted.placeholder": "Date submitted", + "search.filters.filter.dateSubmitted.placeholder": "Tallennnusajankohta", // "search.filters.filter.discoverable.head": "Private", // TODO New key - Add a translation @@ -3858,112 +3243,85 @@ "search.filters.filter.withdrawn.head": "Withdrawn", // "search.filters.filter.entityType.head": "Item Type", - // TODO New key - Add a translation - "search.filters.filter.entityType.head": "Item Type", + "search.filters.filter.entityType.head": "Tietueen tyyppi", // "search.filters.filter.entityType.placeholder": "Item Type", - // TODO New key - Add a translation - "search.filters.filter.entityType.placeholder": "Item Type", + "search.filters.filter.entityType.placeholder": "Tietueen tyyppi", // "search.filters.filter.has_content_in_original_bundle.head": "Has files", - // TODO New key - Add a translation - "search.filters.filter.has_content_in_original_bundle.head": "Has files", + "search.filters.filter.has_content_in_original_bundle.head": "Tiedostot:", // "search.filters.filter.itemtype.head": "Type", - // TODO New key - Add a translation - "search.filters.filter.itemtype.head": "Type", + "search.filters.filter.itemtype.head": "Tyyppi", // "search.filters.filter.itemtype.placeholder": "Type", - // TODO New key - Add a translation - "search.filters.filter.itemtype.placeholder": "Type", + "search.filters.filter.itemtype.placeholder": "Tyyppi", // "search.filters.filter.jobTitle.head": "Job Title", - // TODO New key - Add a translation - "search.filters.filter.jobTitle.head": "Job Title", + "search.filters.filter.jobTitle.head": "Tehtävänimike", // "search.filters.filter.jobTitle.placeholder": "Job Title", - // TODO New key - Add a translation - "search.filters.filter.jobTitle.placeholder": "Job Title", + "search.filters.filter.jobTitle.placeholder": "Tehtävänimike", // "search.filters.filter.knowsLanguage.head": "Known language", - // TODO New key - Add a translation - "search.filters.filter.knowsLanguage.head": "Known language", + "search.filters.filter.knowsLanguage.head": "Tunnettu kieli", // "search.filters.filter.knowsLanguage.placeholder": "Known language", - // TODO New key - Add a translation - "search.filters.filter.knowsLanguage.placeholder": "Known language", + "search.filters.filter.knowsLanguage.placeholder": "Tunnettu kieli", // "search.filters.filter.namedresourcetype.head": "Status", - // TODO New key - Add a translation - "search.filters.filter.namedresourcetype.head": "Status", + "search.filters.filter.namedresourcetype.head": "Tila", // "search.filters.filter.namedresourcetype.placeholder": "Status", - // TODO New key - Add a translation - "search.filters.filter.namedresourcetype.placeholder": "Status", + "search.filters.filter.namedresourcetype.placeholder": "Tila", // "search.filters.filter.objectpeople.head": "People", - // TODO New key - Add a translation - "search.filters.filter.objectpeople.head": "People", + "search.filters.filter.objectpeople.head": "Käyttäjät", // "search.filters.filter.objectpeople.placeholder": "People", - // TODO New key - Add a translation - "search.filters.filter.objectpeople.placeholder": "People", + "search.filters.filter.objectpeople.placeholder": "Käyttäjät", // "search.filters.filter.organizationAddressCountry.head": "Country", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressCountry.head": "Country", + "search.filters.filter.organizationAddressCountry.head": "Maa", // "search.filters.filter.organizationAddressCountry.placeholder": "Country", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressCountry.placeholder": "Country", + "search.filters.filter.organizationAddressCountry.placeholder": "Maa", // "search.filters.filter.organizationAddressLocality.head": "City", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressLocality.head": "City", + "search.filters.filter.organizationAddressLocality.head": "Kaupunki", // "search.filters.filter.organizationAddressLocality.placeholder": "City", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressLocality.placeholder": "City", + "search.filters.filter.organizationAddressLocality.placeholder": "Kaupunki", // "search.filters.filter.organizationFoundingDate.head": "Date Founded", - // TODO New key - Add a translation - "search.filters.filter.organizationFoundingDate.head": "Date Founded", + "search.filters.filter.organizationFoundingDate.head": "Perustamispäivämäärä", // "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", - // TODO New key - Add a translation - "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", + "search.filters.filter.organizationFoundingDate.placeholder": "Perustamispäivämäärä", // "search.filters.filter.scope.head": "Scope", - // TODO New key - Add a translation - "search.filters.filter.scope.head": "Scope", + "search.filters.filter.scope.head": "Rajaus", // "search.filters.filter.scope.placeholder": "Scope filter", - // TODO New key - Add a translation - "search.filters.filter.scope.placeholder": "Scope filter", + "search.filters.filter.scope.placeholder": "Haun tarkennus", // "search.filters.filter.show-less": "Collapse", - // TODO New key - Add a translation - "search.filters.filter.show-less": "Collapse", + "search.filters.filter.show-less": "Sulje", // "search.filters.filter.show-more": "Show more", - // TODO New key - Add a translation - "search.filters.filter.show-more": "Show more", + "search.filters.filter.show-more": "Näytä lisää", // "search.filters.filter.subject.head": "Subject", - // TODO New key - Add a translation - "search.filters.filter.subject.head": "Subject", + "search.filters.filter.subject.head": "Aihe", // "search.filters.filter.subject.placeholder": "Subject", - // TODO New key - Add a translation - "search.filters.filter.subject.placeholder": "Subject", + "search.filters.filter.subject.placeholder": "Aihe", // "search.filters.filter.submitter.head": "Submitter", - // TODO New key - Add a translation - "search.filters.filter.submitter.head": "Submitter", + "search.filters.filter.submitter.head": "Julkaisija", // "search.filters.filter.submitter.placeholder": "Submitter", - // TODO New key - Add a translation - "search.filters.filter.submitter.placeholder": "Submitter", + "search.filters.filter.submitter.placeholder": "Julkaisija", @@ -4005,40 +3363,32 @@ // "search.filters.head": "Filters", - // TODO New key - Add a translation - "search.filters.head": "Filters", + "search.filters.head": "Suodattimet", // "search.filters.reset": "Reset filters", - // TODO New key - Add a translation - "search.filters.reset": "Reset filters", + "search.filters.reset": "Tyhjennä suodattimet", // "search.form.search": "Search", - // TODO New key - Add a translation - "search.form.search": "Search", + "search.form.search": "Hae", // "search.form.search_dspace": "Search DSpace", - // TODO New key - Add a translation - "search.form.search_dspace": "Search DSpace", + "search.form.search_dspace": "Hae arkistosta", // "search.form.search_mydspace": "Search MyDSpace", - // TODO New key - Add a translation - "search.form.search_mydspace": "Search MyDSpace", + "search.form.search_mydspace": "Hae omista tiedoista", // "search.results.head": "Search Results", - // TODO New key - Add a translation - "search.results.head": "Search Results", + "search.results.head": "Hakutulokset", // "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", - // TODO New key - Add a translation - "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", + "search.results.no-results": "Ei tuloksia. Jos sinulla on hakuongelmia, voit", // "search.results.no-results-link": "quotes around it", - // TODO New key - Add a translation - "search.results.no-results-link": "quotes around it", + "search.results.no-results-link": "käyttää lainausmerkkejä", // "search.results.empty": "Your search returned no results.", // TODO New key - Add a translation @@ -4047,102 +3397,79 @@ // "search.sidebar.close": "Back to results", - // TODO New key - Add a translation - "search.sidebar.close": "Back to results", + "search.sidebar.close": "Palaa tuloksiin", // "search.sidebar.filters.title": "Filters", - // TODO New key - Add a translation - "search.sidebar.filters.title": "Filters", + "search.sidebar.filters.title": "Suodattimet", // "search.sidebar.open": "Search Tools", - // TODO New key - Add a translation - "search.sidebar.open": "Search Tools", + "search.sidebar.open": "Hakutyökalut", // "search.sidebar.results": "results", - // TODO New key - Add a translation - "search.sidebar.results": "results", + "search.sidebar.results": "tulokset", // "search.sidebar.settings.rpp": "Results per page", - // TODO New key - Add a translation - "search.sidebar.settings.rpp": "Results per page", + "search.sidebar.settings.rpp": "Tulosta sivulla", // "search.sidebar.settings.sort-by": "Sort By", - // TODO New key - Add a translation - "search.sidebar.settings.sort-by": "Sort By", + "search.sidebar.settings.sort-by": "Järjestä", // "search.sidebar.settings.title": "Settings", - // TODO New key - Add a translation - "search.sidebar.settings.title": "Settings", + "search.sidebar.settings.title": "Asetukset", // "search.view-switch.show-detail": "Show detail", - // TODO New key - Add a translation - "search.view-switch.show-detail": "Show detail", + "search.view-switch.show-detail": "Näytä lisätiedot", // "search.view-switch.show-grid": "Show as grid", - // TODO New key - Add a translation - "search.view-switch.show-grid": "Show as grid", + "search.view-switch.show-grid": "Näydä ruudukkona", // "search.view-switch.show-list": "Show as list", - // TODO New key - Add a translation - "search.view-switch.show-list": "Show as list", + "search.view-switch.show-list": "Näytä listana", // "sorting.dc.title.ASC": "Title Ascending", - // TODO New key - Add a translation - "sorting.dc.title.ASC": "Title Ascending", + "sorting.dc.title.ASC": "Nimeke (A-Ö)", // "sorting.dc.title.DESC": "Title Descending", - // TODO New key - Add a translation - "sorting.dc.title.DESC": "Title Descending", + "sorting.dc.title.DESC": "Nimeke (Ö-A)", // "sorting.score.DESC": "Relevance", - // TODO New key - Add a translation - "sorting.score.DESC": "Relevance", + "sorting.score.DESC": "Relevanssi", // "submission.edit.title": "Edit Submission", - // TODO New key - Add a translation - "submission.edit.title": "Edit Submission", + "submission.edit.title": "Muokkaa julkaisua", // "submission.general.cannot_submit": "You have not the privilege to make a new submission.", - // TODO New key - Add a translation - "submission.general.cannot_submit": "You have not the privilege to make a new submission.", + "submission.general.cannot_submit": "Sinulla ei ole oikeuksia aineiston julkaisemiseen.", // "submission.general.deposit": "Deposit", - // TODO New key - Add a translation - "submission.general.deposit": "Deposit", + "submission.general.deposit": "Tallenna", // "submission.general.discard.confirm.cancel": "Cancel", - // TODO New key - Add a translation - "submission.general.discard.confirm.cancel": "Cancel", + "submission.general.discard.confirm.cancel": "Peruuta", // "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", - // TODO New key - Add a translation - "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", + "submission.general.discard.confirm.info": "Tätä operaatiota ei voi perua. Oletko varma?", // "submission.general.discard.confirm.submit": "Yes, I'm sure", - // TODO New key - Add a translation - "submission.general.discard.confirm.submit": "Yes, I'm sure", + "submission.general.discard.confirm.submit": "Kyllä, olen varma", // "submission.general.discard.confirm.title": "Discard submission", - // TODO New key - Add a translation - "submission.general.discard.confirm.title": "Discard submission", + "submission.general.discard.confirm.title": "Hylkää julkaisu", // "submission.general.discard.submit": "Discard", - // TODO New key - Add a translation - "submission.general.discard.submit": "Discard", + "submission.general.discard.submit": "Hylkää", // "submission.general.save": "Save", - // TODO New key - Add a translation - "submission.general.save": "Save", + "submission.general.save": "Tallenna", // "submission.general.save-later": "Save for later", - // TODO New key - Add a translation - "submission.general.save-later": "Save for later", + "submission.general.save-later": "Tallenna myöhemmäksi", @@ -4423,312 +3750,242 @@ "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Use only for this submission", // "submission.sections.general.add-more": "Add more", - // TODO New key - Add a translation - "submission.sections.general.add-more": "Add more", + "submission.sections.general.add-more": "Lisää enemmän", // "submission.sections.general.collection": "Collection", - // TODO New key - Add a translation - "submission.sections.general.collection": "Collection", + "submission.sections.general.collection": "Kokoelma", // "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", - // TODO New key - Add a translation - "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", + "submission.sections.general.deposit_error_notice": "Ongelma tietueen julkaisemisessa, yritä myöhemmin uudelleen.", // "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", - // TODO New key - Add a translation - "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", + "submission.sections.general.deposit_success_notice": "Julkaisu tallennettu.", // "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", - // TODO New key - Add a translation - "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", + "submission.sections.general.discard_error_notice": "Ongelma tietueen hylkäämisessä, yritä myöhemmin uudelleen.", // "submission.sections.general.discard_success_notice": "Submission discarded successfully.", - // TODO New key - Add a translation - "submission.sections.general.discard_success_notice": "Submission discarded successfully.", + "submission.sections.general.discard_success_notice": "Julkaisu hylätty.", // "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", - // TODO New key - Add a translation - "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", + "submission.sections.general.metadata-extracted": "Uusi metadata poimittu ja lisätty {{sectionId}}-osioon.", // "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", - // TODO New key - Add a translation - "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", + "submission.sections.general.metadata-extracted-new-section": "Uusi {{sectionId}}-osio lisätty julkaisuun.", // "submission.sections.general.no-collection": "No collection found", - // TODO New key - Add a translation - "submission.sections.general.no-collection": "No collection found", + "submission.sections.general.no-collection": "Ei kokoelmia", // "submission.sections.general.no-sections": "No options available", - // TODO New key - Add a translation - "submission.sections.general.no-sections": "No options available", + "submission.sections.general.no-sections": "Ei vaihtoehtoja", // "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", - // TODO New key - Add a translation - "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", + "submission.sections.general.save_error_notice": "Ongelma tietueen tallentamisessa, yritä myöhemmin uudelleen.", // "submission.sections.general.save_success_notice": "Submission saved successfully.", - // TODO New key - Add a translation - "submission.sections.general.save_success_notice": "Submission saved successfully.", + "submission.sections.general.save_success_notice": "Julkaisun tallennus onnistui.", // "submission.sections.general.search-collection": "Search for a collection", - // TODO New key - Add a translation - "submission.sections.general.search-collection": "Search for a collection", + "submission.sections.general.search-collection": "Hae kokoelmaa", // "submission.sections.general.sections_not_valid": "There are incomplete sections.", - // TODO New key - Add a translation - "submission.sections.general.sections_not_valid": "There are incomplete sections.", + "submission.sections.general.sections_not_valid": "Tallennuksessa keskeneräisiä osioita.", // "submission.sections.submit.progressbar.cclicense": "Creative commons license", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.cclicense": "Creative commons license", + "submission.sections.submit.progressbar.cclicense": "Creative commons -lisenssi", // "submission.sections.submit.progressbar.describe.recycle": "Recycle", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.recycle": "Recycle", + "submission.sections.submit.progressbar.describe.recycle": "Kierrätä", // "submission.sections.submit.progressbar.describe.stepcustom": "Describe", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.stepcustom": "Describe", + "submission.sections.submit.progressbar.describe.stepcustom": "Kuvaile", // "submission.sections.submit.progressbar.describe.stepone": "Describe", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.stepone": "Describe", + "submission.sections.submit.progressbar.describe.stepone": "Kuvaile", // "submission.sections.submit.progressbar.describe.steptwo": "Describe", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.steptwo": "Describe", + "submission.sections.submit.progressbar.describe.steptwo": "Kuvaile", // "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", + "submission.sections.submit.progressbar.detect-duplicate": "Mahdollisia kaksoiskappaleita", // "submission.sections.submit.progressbar.license": "Deposit license", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.license": "Deposit license", + "submission.sections.submit.progressbar.license": "Tallennuslisenssi", // "submission.sections.submit.progressbar.upload": "Upload files", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.upload": "Upload files", + "submission.sections.submit.progressbar.upload": "Lataa tiedostoja", // "submission.sections.upload.delete.confirm.cancel": "Cancel", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.cancel": "Cancel", + "submission.sections.upload.delete.confirm.cancel": "Peruuta", // "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", + "submission.sections.upload.delete.confirm.info": "Tätä toimintoa ei voi peruuttaa. Oletko varma?", // "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", + "submission.sections.upload.delete.confirm.submit": "Kyllä, olen varma", // "submission.sections.upload.delete.confirm.title": "Delete bitstream", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.title": "Delete bitstream", + "submission.sections.upload.delete.confirm.title": "Poista tiedosto", // "submission.sections.upload.delete.submit": "Delete", - // TODO New key - Add a translation - "submission.sections.upload.delete.submit": "Delete", + "submission.sections.upload.delete.submit": "Poista", // "submission.sections.upload.drop-message": "Drop files to attach them to the item", - // TODO New key - Add a translation - "submission.sections.upload.drop-message": "Drop files to attach them to the item", + "submission.sections.upload.drop-message": "Pudota tiedostot liittääksesi ne tietueeseen", // "submission.sections.upload.form.access-condition-label": "Access condition type", - // TODO New key - Add a translation - "submission.sections.upload.form.access-condition-label": "Access condition type", + "submission.sections.upload.form.access-condition-label": "Pääsyoikeustyyppi", // "submission.sections.upload.form.date-required": "Date is required.", - // TODO New key - Add a translation - "submission.sections.upload.form.date-required": "Date is required.", + "submission.sections.upload.form.date-required": "Päivämäärä on pakollinen tieto.", // "submission.sections.upload.form.from-label": "Access grant from", - // TODO New key - Add a translation - "submission.sections.upload.form.from-label": "Access grant from", + "submission.sections.upload.form.from-label": "Pääsyoikeus alkaa", // "submission.sections.upload.form.from-placeholder": "From", - // TODO New key - Add a translation - "submission.sections.upload.form.from-placeholder": "From", + "submission.sections.upload.form.from-placeholder": "Lähettäjä", // "submission.sections.upload.form.group-label": "Group", - // TODO New key - Add a translation - "submission.sections.upload.form.group-label": "Group", + "submission.sections.upload.form.group-label": "Ryhmä", // "submission.sections.upload.form.group-required": "Group is required.", - // TODO New key - Add a translation - "submission.sections.upload.form.group-required": "Group is required.", + "submission.sections.upload.form.group-required": "Ryhmä on pakollinen tieto.", // "submission.sections.upload.form.until-label": "Access grant until", - // TODO New key - Add a translation - "submission.sections.upload.form.until-label": "Access grant until", + "submission.sections.upload.form.until-label": "Pääsyoikeus päättyy", // "submission.sections.upload.form.until-placeholder": "Until", - // TODO New key - Add a translation - "submission.sections.upload.form.until-placeholder": "Until", + "submission.sections.upload.form.until-placeholder": "Asti", // "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", - // TODO New key - Add a translation - "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", + "submission.sections.upload.header.policy.default.nolist": "{{collectionName}}-kokoelmaan ladatut tiedostot ovat seuraavien ryhmien saatavilla:", // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", - // TODO New key - Add a translation - "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", + "submission.sections.upload.header.policy.default.withlist": "Yksittäisten tiedostojen pääsyrajoitusten lisäksi {{collectionName}}-kokoelmaan ladatut tiedostot ovat seuraavien ryhmien saatavilla:", // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", - // TODO New key - Add a translation - "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + "submission.sections.upload.info": "Tietueen kaikki tiedostot on listattu tässä. Voit päivittää tiedoston metadataa ja pääsyehtoja tai ladata lisää tiedostoja raahaamalla ne mihin hyvänsä sivun kohtaan", // "submission.sections.upload.no-entry": "No", - // TODO New key - Add a translation - "submission.sections.upload.no-entry": "No", + "submission.sections.upload.no-entry": "Ei", // "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", - // TODO New key - Add a translation - "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", + "submission.sections.upload.no-file-uploaded": "Tiedostoa ei vielä ladattu.", // "submission.sections.upload.save-metadata": "Save metadata", - // TODO New key - Add a translation - "submission.sections.upload.save-metadata": "Save metadata", + "submission.sections.upload.save-metadata": "Tallenna metadata", // "submission.sections.upload.undo": "Cancel", - // TODO New key - Add a translation - "submission.sections.upload.undo": "Cancel", + "submission.sections.upload.undo": "Peruuta", // "submission.sections.upload.upload-failed": "Upload failed", - // TODO New key - Add a translation - "submission.sections.upload.upload-failed": "Upload failed", + "submission.sections.upload.upload-failed": "Lataus epäonnistui", // "submission.sections.upload.upload-successful": "Upload successful", - // TODO New key - Add a translation - "submission.sections.upload.upload-successful": "Upload successful", + "submission.sections.upload.upload-successful": "Lataus valmis", // "submission.submit.title": "Submission", - // TODO New key - Add a translation - "submission.submit.title": "Submission", + "submission.submit.title": "Julkaisu", // "submission.workflow.generic.delete": "Delete", - // TODO New key - Add a translation - "submission.workflow.generic.delete": "Delete", + "submission.workflow.generic.delete": "Poista", // "submission.workflow.generic.delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.", // TODO New key - Add a translation "submission.workflow.generic.delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.", // "submission.workflow.generic.edit": "Edit", - // TODO New key - Add a translation - "submission.workflow.generic.edit": "Edit", + "submission.workflow.generic.edit": "Muokkaa", // "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", - // TODO New key - Add a translation - "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", + "submission.workflow.generic.edit-help": "Valitse tämä muuttaaksesi tietueen metadataa.", // "submission.workflow.generic.view": "View", - // TODO New key - Add a translation - "submission.workflow.generic.view": "View", + "submission.workflow.generic.view": "Näytä", // "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", - // TODO New key - Add a translation - "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", + "submission.workflow.generic.view-help": "Valitse tämä katsoaksesi tietueen metadataa.", // "submission.workflow.tasks.claimed.approve": "Approve", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.approve": "Approve", + "submission.workflow.tasks.claimed.approve": "Hyväksy", // "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", // TODO New key - Add a translation "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", // "submission.workflow.tasks.claimed.edit": "Edit", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.edit": "Edit", + "submission.workflow.tasks.claimed.edit": "Muokkaa", // "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", + "submission.workflow.tasks.claimed.edit_help": "Valitse tämä muuttaaksesi tietueen metadataa.", // "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", + "submission.workflow.tasks.claimed.reject.reason.info": "Syötä kenttään syy julkaisun hylkäämiselle. Kerro myös, voiko julkaisija korjata ongelman ja lähettää aineiston uudelleen.", // "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", + "submission.workflow.tasks.claimed.reject.reason.placeholder": "Kuvaa hylkäyksen syy", // "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", + "submission.workflow.tasks.claimed.reject.reason.submit": "Hylkää tietue", // "submission.workflow.tasks.claimed.reject.reason.title": "Reason", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.title": "Reason", + "submission.workflow.tasks.claimed.reject.reason.title": "Syy", // "submission.workflow.tasks.claimed.reject.submit": "Reject", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.submit": "Reject", + "submission.workflow.tasks.claimed.reject.submit": "Hylkää", // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", // TODO New key - Add a translation "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", // "submission.workflow.tasks.claimed.return": "Return to pool", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.return": "Return to pool", + "submission.workflow.tasks.claimed.return": "Palauta tehtäväjonoon", // "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", + "submission.workflow.tasks.claimed.return_help": "Palauta tehtävä tehtäväjonoon, jotta toinen käyttäjä voi suorittaa sen.", // "submission.workflow.tasks.generic.error": "Error occurred during operation...", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.error": "Error occurred during operation...", + "submission.workflow.tasks.generic.error": "Virhe toimintoa suoritettaessa...", // "submission.workflow.tasks.generic.processing": "Processing...", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.processing": "Processing...", + "submission.workflow.tasks.generic.processing": "Käsitellään...", // "submission.workflow.tasks.generic.submitter": "Submitter", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.submitter": "Submitter", + "submission.workflow.tasks.generic.submitter": "Julkaisija", // "submission.workflow.tasks.generic.success": "Operation successful", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.success": "Operation successful", + "submission.workflow.tasks.generic.success": "Toiminto onnistui", // "submission.workflow.tasks.pool.claim": "Claim", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.claim": "Claim", + "submission.workflow.tasks.pool.claim": "Ota itsellesi", // "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", + "submission.workflow.tasks.pool.claim_help": "Ota tehtävä itsellesi.", // "submission.workflow.tasks.pool.hide-detail": "Hide detail", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.hide-detail": "Hide detail", + "submission.workflow.tasks.pool.hide-detail": "Piilota lisätiedot", // "submission.workflow.tasks.pool.show-detail": "Show detail", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.show-detail": "Show detail", + "submission.workflow.tasks.pool.show-detail": "Näytä lisätiedot", // "title": "DSpace", - // TODO New key - Add a translation - "title": "DSpace", + "title": "Julkaisuarkisto", // "administrativeView.search.results.head": "Administrative Search", @@ -4742,24 +3999,19 @@ // "uploader.browse": "browse", - // TODO New key - Add a translation - "uploader.browse": "browse", + "uploader.browse": "selaa", // "uploader.drag-message": "Drag & Drop your files here", - // TODO New key - Add a translation - "uploader.drag-message": "Drag & Drop your files here", + "uploader.drag-message": "Raahaa tiedostot tähän", // "uploader.or": ", or", - // TODO New key - Add a translation - "uploader.or": ", or", + "uploader.or": ", tai", // "uploader.processing": "Processing", - // TODO New key - Add a translation - "uploader.processing": "Processing", + "uploader.processing": "Käsitellään", // "uploader.queue-length": "Queue length", - // TODO New key - Add a translation - "uploader.queue-length": "Queue length", + "uploader.queue-length": "Jonon pituus", // "virtual-metadata.delete-item.info": "Select the types for which you want to save the virtual metadata as real metadata", // TODO New key - Add a translation @@ -4775,4 +4027,4 @@ -} \ No newline at end of file +} From 92b1a4477dfdb2e12ad6903a4d79ef3ed4e44cab Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 27 May 2020 15:54:33 -0500 Subject: [PATCH 073/103] Fix Docker local.cfg by correcting UI port to 4000 --- docker/local.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/local.cfg b/docker/local.cfg index 70bc45c112..a511c25789 100644 --- a/docker/local.cfg +++ b/docker/local.cfg @@ -1,5 +1,6 @@ dspace.dir=/dspace db.url=jdbc:postgresql://dspacedb:5432/dspace dspace.server.url=http://localhost:8080/server +dspace.ui.url=http://localhost:4000 dspace.name=DSpace Started with Docker Compose solr.server=http://dspacesolr:8983/solr From e4a83f0704ef3f9b32bd8e9ad50361818141f107 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 15 May 2020 17:27:02 +0200 Subject: [PATCH 074/103] fixed ui environment vars issue --- package.json | 2 +- scripts/serve.ts | 8 ++ server.ts | 200 +++++++++++++++++++++++++++++++++-------------- src/routes.ts | 15 ---- 4 files changed, 150 insertions(+), 75 deletions(-) create mode 100644 scripts/serve.ts delete mode 100644 src/routes.ts diff --git a/package.json b/package.json index 4c6bd31cac..6b87db247a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "pree2e": "yarn run config:prod", "pree2e:ci": "yarn run config:prod", "start": "yarn run start:prod", - "serve": "ng serve", + "serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts", "start:dev": "npm-run-all --parallel config:dev:watch serve", "start:prod": "yarn run build:prod && yarn run serve:ssr", "build": "ng build", diff --git a/scripts/serve.ts b/scripts/serve.ts new file mode 100644 index 0000000000..ff3554804e --- /dev/null +++ b/scripts/serve.ts @@ -0,0 +1,8 @@ +import { environment } from '../src/environments/environment'; + +import * as child from 'child_process'; + +child.spawn( + `ng serve --host ${environment.ui.host} --port ${environment.ui.port} --servePath ${environment.ui.nameSpace} --ssl ${environment.ui.ssl}`, + { stdio:'inherit', shell: true } +); diff --git a/server.ts b/server.ts index 31cefe4ec5..131b6bb4ae 100644 --- a/server.ts +++ b/server.ts @@ -1,75 +1,157 @@ -/** - * *** NOTE ON IMPORTING FROM ANGULAR AND NGUNIVERSAL IN THIS FILE *** - * - * If your application uses third-party dependencies, you'll need to - * either use Webpack or the Angular CLI's `bundleDependencies` feature - * in order to adequately package them for use on the server without a - * node_modules directory. - * - * However, due to the nature of the CLI's `bundleDependencies`, importing - * Angular in this file will create a different instance of Angular than - * the version in the compiled application code. This leads to unavoidable - * conflicts. Therefore, please do not explicitly import from @angular or - * @nguniversal in this file. You can export any needed resources - * from your application's main.server.ts file, as seen below with the - * import for `ngExpressEngine`. - */ - import 'zone.js/dist/zone-node'; import 'reflect-metadata'; +import 'rxjs'; +import * as fs from 'fs'; +import * as pem from 'pem'; +import * as https from 'https'; +import * as morgan from 'morgan'; import * as express from 'express'; -import { join } from 'path'; -import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import * as bodyParser from 'body-parser'; +import * as compression from 'compression'; import * as cookieParser from 'cookie-parser'; + +import { enableProdMode, NgModuleFactory, Type } from '@angular/core'; + +import { ngExpressEngine } from '@nguniversal/express-engine'; + +import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; -// Express server -const app = express(); +export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) { + const app = express(); -const PORT = environment.ui.port || 4000; -const DIST_FOLDER = join(process.cwd(), 'dist/browser'); + if (environment.production) { + enableProdMode(); + app.use(compression()); + } -// * NOTE :: leave this as require() since this file is built Dynamically from webpack -const { ServerAppModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main'); + app.use(morgan('dev')); -app.use(cookieParser()); -app.use(bodyParser.json()); + app.use(cookieParser()); + app.use(bodyParser.json()); -app.engine('html', (_, options, callback) => - ngExpressEngine({ - bootstrap: ServerAppModuleNgFactory, - providers: [ - { - provide: REQUEST, - useValue: (options as any).req, - }, - { - provide: RESPONSE, - useValue: (options as any).req.res, - }, - provideModuleMap(LAZY_MODULE_MAP) - ], - })(_, options, callback) -); + app.engine('html', (_, options, callback) => + ngExpressEngine({ + bootstrap: bootstrap, + providers: [ + { + provide: REQUEST, + useValue: (options as any).req, + }, + { + provide: RESPONSE, + useValue: (options as any).req.res, + }, + ], + })(_, (options as any), callback) + ); -app.set('view engine', 'html'); -app.set('views', DIST_FOLDER); + app.set('view engine', 'ejs'); + app.set('view engine', 'html'); + app.set('views', 'src'); -// Example Express Rest API endpoints -// app.get('/api/**', (req, res) => { }); -// Serve static files from /browser -app.get('*.*', express.static(DIST_FOLDER, { - maxAge: '1y' -})); + function cacheControl(req, res, next) { + // instruct browser to revalidate + res.header('Cache-Control', environment.cache.control || 'max-age=60'); + next(); + } -// All regular routes use the Universal engine -app.get('*', (req, res) => { - res.render('index', { req }); -}); + app.use('/', cacheControl, express.static('dist', { index: false })); -// Start up the Node server -app.listen(PORT, () => { - console.log(`Node Express server listening on http://localhost:${PORT}`); -}); +// TODO: either remove or update mock backend +// app.get('/data.json', serverApi); +// app.use('/api', createMockApi()); + + function ngApp(req, res) { + const dspace = { + originalRequest: { + headers: req.headers, + body: req.body, + method: req.method, + params: req.params, + reportProgress: req.reportProgress, + withCredentials: req.withCredentials, + responseType: req.responseType, + urlWithParams: req.urlWithParams + } + }; + + function onHandleError(parentZoneDelegate, currentZone, targetZone, error) { + if (!res._headerSent) { + console.warn('Error in SSR, serving for direct CSR. Error details : ', error); + res.sendFile('index.csr.html', { root: './src' }); + } + } + + if (environment.universal.preboot) { + Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => { + res.render('../dist/index.html', { + req, + res, + preboot: environment.universal.preboot, + async: environment.universal.async, + time: environment.universal.time, + baseUrl: environment.ui.nameSpace, + originUrl: environment.ui.baseUrl, + requestUrl: req.originalUrl + }); + }); + } else { + console.log('Universal off, serving for direct CSR'); + res.render('index-csr.ejs', { + root: './src', + scripts: `` + }); + } + } + + function serverStarted() { + console.log(`[${new Date().toTimeString()}] Listening at ${environment.ui.baseUrl}`); + } + + function createHttpsServer(keys) { + https.createServer({ + key: keys.serviceKey, + cert: keys.certificate + }, app).listen(environment.ui.port, environment.ui.host, () => { + serverStarted(); + }); + } + + if (environment.ui.ssl) { + let serviceKey; + try { + serviceKey = fs.readFileSync('./config/ssl/key.pem'); + } catch (e) { + console.warn('Service key not found at ./config/ssl/key.pem'); + } + + let certificate; + try { + certificate = fs.readFileSync('./config/ssl/cert.pem'); + } catch (e) { + console.warn('Certificate not found at ./config/ssl/key.pem'); + } + + if (serviceKey && certificate) { + createHttpsServer({ + serviceKey: serviceKey, + certificate: certificate + }); + } else { + + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + + pem.createCertificate({ + days: 1, + selfSigned: true + }, (error, keys) => { + createHttpsServer(keys); + }); + } + } else { + app.listen(environment.ui.port, environment.ui.host, () => { + serverStarted(); + }); + }} diff --git a/src/routes.ts b/src/routes.ts deleted file mode 100644 index f3e963b25a..0000000000 --- a/src/routes.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const ROUTES: string[] = [ - 'home', - 'items/:id', - 'login', - 'logout', - 'collections/:id', - 'communities/:id', - 'login', - 'logout', - 'search', - 'submit', - 'workspaceitems/:id/edit', - 'workflowitems/:id/edit', - '**' -]; From e7043f87345180f79188b95c3513f0157865a2dc Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 18 May 2020 11:24:00 +0200 Subject: [PATCH 075/103] changes to server.ts --- server.ts | 261 +++++++++++++++++++++++++++++------------------------- 1 file changed, 142 insertions(+), 119 deletions(-) diff --git a/server.ts b/server.ts index 131b6bb4ae..f1e8e1943f 100644 --- a/server.ts +++ b/server.ts @@ -1,3 +1,20 @@ +/** + * *** NOTE ON IMPORTING FROM ANGULAR AND NGUNIVERSAL IN THIS FILE *** + * + * If your application uses third-party dependencies, you'll need to + * either use Webpack or the Angular CLI's `bundleDependencies` feature + * in order to adequately package them for use on the server without a + * node_modules directory. + * + * However, due to the nature of the CLI's `bundleDependencies`, importing + * Angular in this file will create a different instance of Angular than + * the version in the compiled application code. This leads to unavoidable + * conflicts. Therefore, please do not explicitly import from @angular or + * @nguniversal in this file. You can export any needed resources + * from your application's main.server.ts file, as seen below with the + * import for `ngExpressEngine`. + */ + import 'zone.js/dist/zone-node'; import 'reflect-metadata'; import 'rxjs'; @@ -10,148 +27,154 @@ import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as compression from 'compression'; import * as cookieParser from 'cookie-parser'; +import { join } from 'path'; import { enableProdMode, NgModuleFactory, Type } from '@angular/core'; -import { ngExpressEngine } from '@nguniversal/express-engine'; - import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; -export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) { - const app = express(); +const DIST_FOLDER = join(process.cwd(), 'dist/browser'); - if (environment.production) { - enableProdMode(); - app.use(compression()); - } +// * NOTE :: leave this as require() since this file is built Dynamically from webpack +const { ServerAppModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main'); - app.use(morgan('dev')); +const app = express(); - app.use(cookieParser()); - app.use(bodyParser.json()); +if (environment.production) { + enableProdMode(); + app.use(compression()); +} - app.engine('html', (_, options, callback) => - ngExpressEngine({ - bootstrap: bootstrap, - providers: [ - { - provide: REQUEST, - useValue: (options as any).req, - }, - { - provide: RESPONSE, - useValue: (options as any).req.res, - }, - ], - })(_, (options as any), callback) - ); +app.use(morgan('dev')); - app.set('view engine', 'ejs'); - app.set('view engine', 'html'); - app.set('views', 'src'); +app.use(cookieParser()); +app.use(bodyParser.json()); - function cacheControl(req, res, next) { - // instruct browser to revalidate - res.header('Cache-Control', environment.cache.control || 'max-age=60'); - next(); - } +app.engine('html', (_, options, callback) => + ngExpressEngine({ + bootstrap: ServerAppModuleNgFactory, + providers: [ + { + provide: REQUEST, + useValue: (options as any).req, + }, + { + provide: RESPONSE, + useValue: (options as any).req.res, + }, + provideModuleMap(LAZY_MODULE_MAP) + ], + })(_, (options as any), callback) +); - app.use('/', cacheControl, express.static('dist', { index: false })); +app.set('view engine', 'ejs'); +app.set('view engine', 'html'); +app.set('views', DIST_FOLDER); + +function cacheControl(req, res, next) { + // instruct browser to revalidate + res.header('Cache-Control', environment.cache.control || 'max-age=60'); + next(); +} + +app.use('/', cacheControl, express.static('dist', { index: false })); // TODO: either remove or update mock backend // app.get('/data.json', serverApi); // app.use('/api', createMockApi()); - function ngApp(req, res) { - const dspace = { - originalRequest: { - headers: req.headers, - body: req.body, - method: req.method, - params: req.params, - reportProgress: req.reportProgress, - withCredentials: req.withCredentials, - responseType: req.responseType, - urlWithParams: req.urlWithParams - } - }; - - function onHandleError(parentZoneDelegate, currentZone, targetZone, error) { - if (!res._headerSent) { - console.warn('Error in SSR, serving for direct CSR. Error details : ', error); - res.sendFile('index.csr.html', { root: './src' }); - } +function ngApp(req, res) { + const dspace = { + originalRequest: { + headers: req.headers, + body: req.body, + method: req.method, + params: req.params, + reportProgress: req.reportProgress, + withCredentials: req.withCredentials, + responseType: req.responseType, + urlWithParams: req.urlWithParams } + }; - if (environment.universal.preboot) { - Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => { - res.render('../dist/index.html', { - req, - res, - preboot: environment.universal.preboot, - async: environment.universal.async, - time: environment.universal.time, - baseUrl: environment.ui.nameSpace, - originUrl: environment.ui.baseUrl, - requestUrl: req.originalUrl - }); - }); - } else { - console.log('Universal off, serving for direct CSR'); - res.render('index-csr.ejs', { - root: './src', - scripts: `` - }); + function onHandleError(parentZoneDelegate, currentZone, targetZone, error) { + if (!res._headerSent) { + console.warn('Error in SSR, serving for direct CSR. Error details : ', error); + res.sendFile('index.csr.html', { root: DIST_FOLDER }); } } - function serverStarted() { - console.log(`[${new Date().toTimeString()}] Listening at ${environment.ui.baseUrl}`); - } - - function createHttpsServer(keys) { - https.createServer({ - key: keys.serviceKey, - cert: keys.certificate - }, app).listen(environment.ui.port, environment.ui.host, () => { - serverStarted(); + if (environment.universal.preboot) { + Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => { + res.render(DIST_FOLDER, { + req, + res, + preboot: environment.universal.preboot, + async: environment.universal.async, + time: environment.universal.time, + baseUrl: environment.ui.nameSpace, + originUrl: environment.ui.baseUrl, + requestUrl: req.originalUrl + }); }); - } - - if (environment.ui.ssl) { - let serviceKey; - try { - serviceKey = fs.readFileSync('./config/ssl/key.pem'); - } catch (e) { - console.warn('Service key not found at ./config/ssl/key.pem'); - } - - let certificate; - try { - certificate = fs.readFileSync('./config/ssl/cert.pem'); - } catch (e) { - console.warn('Certificate not found at ./config/ssl/key.pem'); - } - - if (serviceKey && certificate) { - createHttpsServer({ - serviceKey: serviceKey, - certificate: certificate - }); - } else { - - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - - pem.createCertificate({ - days: 1, - selfSigned: true - }, (error, keys) => { - createHttpsServer(keys); - }); - } } else { - app.listen(environment.ui.port, environment.ui.host, () => { - serverStarted(); + console.log('Universal off, serving for direct CSR'); + res.render('index-csr.ejs', { + root: DIST_FOLDER, + scripts: `` }); - }} + } +} + +app.get('*.*', ngApp); + +function serverStarted() { + console.log(`[${new Date().toTimeString()}] Listening at ${environment.ui.baseUrl}`); +} + +function createHttpsServer(keys) { + https.createServer({ + key: keys.serviceKey, + cert: keys.certificate + }, app).listen(environment.ui.port, environment.ui.host, () => { + serverStarted(); + }); +} + +if (environment.ui.ssl) { + let serviceKey; + try { + serviceKey = fs.readFileSync('./config/ssl/key.pem'); + } catch (e) { + console.warn('Service key not found at ./config/ssl/key.pem'); + } + + let certificate; + try { + certificate = fs.readFileSync('./config/ssl/cert.pem'); + } catch (e) { + console.warn('Certificate not found at ./config/ssl/key.pem'); + } + + if (serviceKey && certificate) { + createHttpsServer({ + serviceKey: serviceKey, + certificate: certificate + }); + } else { + + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + + pem.createCertificate({ + days: 1, + selfSigned: true + }, (error, keys) => { + createHttpsServer(keys); + }); + } +} else { + app.listen(environment.ui.port, environment.ui.host, () => { + serverStarted(); + }); +} From f881c2f428324ab1d5dd0dd586bf3ddce0313397 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 18 May 2020 16:20:09 +0200 Subject: [PATCH 076/103] fixed SSR issues --- angular.json | 2 +- server.ts | 6 +++--- src/styles/_bootstrap_variables.scss | 2 +- tsconfig.server.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/angular.json b/angular.json index b3fbd82f02..92c0f27d2b 100644 --- a/angular.json +++ b/angular.json @@ -21,7 +21,7 @@ "path": "./webpack/webpack.common.ts", "mergeStrategies": { "loaders": "prepend" - } + }, }, "outputPath": "dist/browser", "index": "src/index.html", diff --git a/server.ts b/server.ts index f1e8e1943f..f13c8ef272 100644 --- a/server.ts +++ b/server.ts @@ -78,7 +78,7 @@ function cacheControl(req, res, next) { next(); } -app.use('/', cacheControl, express.static('dist', { index: false })); +app.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false })); // TODO: either remove or update mock backend // app.get('/data.json', serverApi); @@ -107,7 +107,7 @@ function ngApp(req, res) { if (environment.universal.preboot) { Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => { - res.render(DIST_FOLDER, { + res.render(DIST_FOLDER + '/index.html', { req, res, preboot: environment.universal.preboot, @@ -127,7 +127,7 @@ function ngApp(req, res) { } } -app.get('*.*', ngApp); +app.get('*', ngApp); function serverStarted() { console.log(`[${new Date().toTimeString()}] Listening at ${environment.ui.baseUrl}`); diff --git a/src/styles/_bootstrap_variables.scss b/src/styles/_bootstrap_variables.scss index 399cc064f3..42f52282dc 100644 --- a/src/styles/_bootstrap_variables.scss +++ b/src/styles/_bootstrap_variables.scss @@ -8,7 +8,7 @@ $sidebar-items-width: 250px !default; $total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width !default; /* Fonts */ -$fa-font-path: "node_modules/@fortawesome/fontawesome-free/webfonts" !default; +$fa-font-path: "/assets/fonts" !default; /* Images */ $image-path: "../assets/images" !default; diff --git a/tsconfig.server.json b/tsconfig.server.json index d3e7ca2f81..1329b32ace 100644 --- a/tsconfig.server.json +++ b/tsconfig.server.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.app.json", "compilerOptions": { "outDir": "./out-tsc/app-server", - "module": "commonjs" + "module": "commonjs", }, "angularCompilerOptions": { "entryModule": "./src/modules/app/server-app.module#ServerAppModule" From a9cb6aeaa617a6f30e175411c1cdb1b2c58b1b54 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 25 May 2020 13:45:11 +0200 Subject: [PATCH 077/103] added doc to scripts --- scripts/serve.ts | 3 ++ server.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/scripts/serve.ts b/scripts/serve.ts index ff3554804e..c69f8e8a21 100644 --- a/scripts/serve.ts +++ b/scripts/serve.ts @@ -2,6 +2,9 @@ import { environment } from '../src/environments/environment'; import * as child from 'child_process'; +/** + * Calls `ng serve` with the following arguments configured for the UI in the environment file: host, port, nameSpace, ssl + */ child.spawn( `ng serve --host ${environment.ui.host} --port ${environment.ui.port} --servePath ${environment.ui.nameSpace} --ssl ${environment.ui.ssl}`, { stdio:'inherit', shell: true } diff --git a/server.ts b/server.ts index f13c8ef272..a5d47d8bd7 100644 --- a/server.ts +++ b/server.ts @@ -34,23 +34,50 @@ import { enableProdMode, NgModuleFactory, Type } from '@angular/core'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; +/* + * Set path for the browser application's dist folder + */ const DIST_FOLDER = join(process.cwd(), 'dist/browser'); // * NOTE :: leave this as require() since this file is built Dynamically from webpack const { ServerAppModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main'); +/* + * Create a new express application + */ const app = express(); +/* + * If production mode is enabled in the environment file: + * - Enable Angular's production mode + * - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression) + */ if (environment.production) { enableProdMode(); app.use(compression()); } +/* + * Enable request logging + * See [morgan](https://github.com/expressjs/morgan) + */ app.use(morgan('dev')); +/* + * Add cookie parser middleware + * See [morgan](https://github.com/expressjs/cookie-parser) + */ app.use(cookieParser()); + +/* + * Add parser for request bodies + * See [morgan](https://github.com/expressjs/body-parser) + */ app.use(bodyParser.json()); +/* + * Render html pages by running angular server side + */ app.engine('html', (_, options, callback) => ngExpressEngine({ bootstrap: ServerAppModuleNgFactory, @@ -68,23 +95,39 @@ app.engine('html', (_, options, callback) => })(_, (options as any), callback) ); +/* + * Register the view engines for html and ejs + */ app.set('view engine', 'ejs'); app.set('view engine', 'html'); + +/* + * Set views folder path to directory where template files are stored + */ app.set('views', DIST_FOLDER); +/* + * Adds a cache control header to the response + * The cache control value can be configured in the environments file and defaults to max-age=60 + */ function cacheControl(req, res, next) { // instruct browser to revalidate res.header('Cache-Control', environment.cache.control || 'max-age=60'); next(); } +/* + * Serve static resources (images, i18n messages, …) + */ app.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false })); -// TODO: either remove or update mock backend -// app.get('/data.json', serverApi); -// app.use('/api', createMockApi()); - +/* + * The callback function to serve server side angular + */ function ngApp(req, res) { + // Object to be set to window.dspace when CSR is used + // this allows us to pass the info in the original request + // to the dspace7-angular instance running in the client's browser const dspace = { originalRequest: { headers: req.headers, @@ -98,14 +141,20 @@ function ngApp(req, res) { } }; + // callback function for the case when SSR throws an error. function onHandleError(parentZoneDelegate, currentZone, targetZone, error) { if (!res._headerSent) { console.warn('Error in SSR, serving for direct CSR. Error details : ', error); - res.sendFile('index.csr.html', { root: DIST_FOLDER }); + res.sendFile('index.csr.ejs', { + root: DIST_FOLDER, + scripts: `` + }); } } if (environment.universal.preboot) { + // If preboot is enabled, create a new zone for SSR, and + // register the error handler for when it throws an error Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => { res.render(DIST_FOLDER + '/index.html', { req, @@ -119,6 +168,8 @@ function ngApp(req, res) { }); }); } else { + // If preboot is disabled, just serve the client side ejs template and pass it the required + // variables console.log('Universal off, serving for direct CSR'); res.render('index-csr.ejs', { root: DIST_FOLDER, @@ -127,12 +178,20 @@ function ngApp(req, res) { } } +// Register the ngApp callback function to handle incoming requests app.get('*', ngApp); +/* + * Callback function for when the server has started + */ function serverStarted() { console.log(`[${new Date().toTimeString()}] Listening at ${environment.ui.baseUrl}`); } +/* + * Create an HTTPS server with the configured port and host + * @param keys SSL credentials + */ function createHttpsServer(keys) { https.createServer({ key: keys.serviceKey, @@ -142,6 +201,13 @@ function createHttpsServer(keys) { }); } +/* + * If SSL is enabled + * - Read credentials from configuration files + * - Call script to start an HTTPS server with these credentials + * When SSL is disabled + * - Start an HTTP server on the configured port and host + */ if (environment.ui.ssl) { let serviceKey; try { From d483d46370568f5d27caba789109534d10af3e7d Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 18 May 2020 14:19:50 +0200 Subject: [PATCH 078/103] 70905: Breadcrumb update issue --- .../breadcrumbs/breadcrumbs.component.html | 31 +++++++++-------- .../breadcrumbs/breadcrumbs.component.spec.ts | 18 ++++++---- src/app/breadcrumbs/breadcrumbs.component.ts | 34 +++++-------------- 3 files changed, 36 insertions(+), 47 deletions(-) diff --git a/src/app/breadcrumbs/breadcrumbs.component.html b/src/app/breadcrumbs/breadcrumbs.component.html index b773964d1e..6c1c31c89f 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.html +++ b/src/app/breadcrumbs/breadcrumbs.component.html @@ -1,17 +1,20 @@ - + + - - - + + + - - - + + + + diff --git a/src/app/breadcrumbs/breadcrumbs.component.spec.ts b/src/app/breadcrumbs/breadcrumbs.component.spec.ts index 6b903e761a..32b5dcf0d0 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.spec.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.spec.ts @@ -9,6 +9,9 @@ import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock'; import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model'; import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service'; import { Breadcrumb } from './breadcrumb/breadcrumb.model'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { VarDirective } from '../shared/utils/var.directive'; import { getTestScheduler } from 'jasmine-marbles'; class TestBreadcrumbsService implements BreadcrumbsService { @@ -64,17 +67,16 @@ describe('BreadcrumbsComponent', () => { beforeEach(async(() => { init(); TestBed.configureTestingModule({ - declarations: [BreadcrumbsComponent], + declarations: [BreadcrumbsComponent, VarDirective], imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: TranslateLoaderMock } - })], + }), NgbModule], providers: [ - { provide: ActivatedRoute, useValue: route } - - ] + {provide: ActivatedRoute, useValue: route} + ], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); })); @@ -92,14 +94,16 @@ describe('BreadcrumbsComponent', () => { describe('ngOnInit', () => { beforeEach(() => { - spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([])) + spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([])); }); it('should call resolveBreadcrumb on init', () => { router.events = observableOf(new NavigationEnd(0, '', '')); component.ngOnInit(); + fixture.detectChanges(); + expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root); - }) + }); }); describe('resolveBreadcrumbs', () => { diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index 2bba3c76b6..af63ec985d 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -1,9 +1,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Breadcrumb } from './breadcrumb/breadcrumb.model'; -import { hasNoValue, hasValue, isNotUndefined, isUndefined } from '../shared/empty.util'; +import { hasNoValue, hasValue, isUndefined } from '../shared/empty.util'; import { filter, map, switchMap, tap } from 'rxjs/operators'; -import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs'; +import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; /** * Component representing the breadcrumbs of a page @@ -13,22 +13,17 @@ import { combineLatest, Observable, Subscription, of as observableOf } from 'rxj templateUrl: './breadcrumbs.component.html', styleUrls: ['./breadcrumbs.component.scss'] }) -export class BreadcrumbsComponent implements OnInit, OnDestroy { +export class BreadcrumbsComponent implements OnInit { /** - * List of breadcrumbs for this page + * Observable of the list of breadcrumbs for this page */ - breadcrumbs: Breadcrumb[]; + breadcrumbs$: Observable; /** * Whether or not to show breadcrumbs on this page */ showBreadcrumbs: boolean; - /** - * Subscription to unsubscribe from on destroy - */ - subscription: Subscription; - constructor( private route: ActivatedRoute, private router: Router @@ -39,14 +34,11 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy { * Sets the breadcrumbs on init for this page */ ngOnInit(): void { - this.subscription = this.router.events.pipe( + this.breadcrumbs$ = this.router.events.pipe( filter((e): e is NavigationEnd => e instanceof NavigationEnd), tap(() => this.reset()), - switchMap(() => this.resolveBreadcrumbs(this.route.root)) - ).subscribe((breadcrumbs) => { - this.breadcrumbs = breadcrumbs; - } - ) + switchMap(() => this.resolveBreadcrumbs(this.route.root)), + ); } /** @@ -81,20 +73,10 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy { return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]); } - /** - * Unsubscribe from subscription - */ - ngOnDestroy(): void { - if (hasValue(this.subscription)) { - this.subscription.unsubscribe(); - } - } - /** * Resets the state of the breadcrumbs */ reset() { - this.breadcrumbs = []; this.showBreadcrumbs = true; } } From 36acb7578f94dbcb4a0b5c5bf2b2433fc3955881 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 19 May 2020 15:38:16 +0200 Subject: [PATCH 079/103] Fix to breadcrumb related injector issues on edit pages --- src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts | 4 +++- src/app/core/breadcrumbs/community-breadcrumb.resolver.ts | 4 +++- src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts | 4 +++- src/app/core/breadcrumbs/dso-breadcrumbs.service.ts | 4 +++- src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts | 4 +++- src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts | 4 +++- src/app/core/breadcrumbs/item-breadcrumb.resolver.ts | 4 +++- 7 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index 7384a031db..80d7563637 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con /** * The class that resolves the BreadcrumbConfig object for a Collection */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) { super(breadcrumbService, dataService); diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index d1f21455f2..298d69133f 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con /** * The class that resolves the BreadcrumbConfig object for a Community */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) { super(breadcrumbService, dataService); diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 80e68a16f5..09292fec21 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -13,7 +13,9 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * The class that resolves the BreadcrumbConfig object for a DSpaceObject */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export abstract class DSOBreadcrumbResolver implements Resolve> { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService) { } diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index 3cb73be876..003c11bf83 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -15,7 +15,9 @@ import { Injectable } from '@angular/core'; /** * Service to calculate DSpaceObject breadcrumbs for a single part of the route */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class DSOBreadcrumbsService implements BreadcrumbsService { constructor( private linkService: LinkService, diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index de7d061a3f..1b8c5bcbd1 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -7,7 +7,9 @@ import { hasNoValue } from '../../shared/empty.util'; /** * The class that resolves a BreadcrumbConfig object with an i18n key string for a route */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class I18nBreadcrumbResolver implements Resolve> { constructor(protected breadcrumbService: I18nBreadcrumbsService) { } diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts index e07d9ed541..b774b58126 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts @@ -11,7 +11,9 @@ export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs'; /** * Service to calculate i18n breadcrumbs for a single part of the route */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class I18nBreadcrumbsService implements BreadcrumbsService { /** diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index cd0c23cf82..8e13eda01d 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con /** * The class that resolves the BreadcrumbConfig object for an Item */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { super(breadcrumbService, dataService); From f0bf87b7f92bcc63910124a18a5c14a978da7a39 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 28 May 2020 11:39:42 +0200 Subject: [PATCH 080/103] Show edit group button only when resource policy has permissions on group --- .../shared/resource-policies/resource-policies.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index 07472ddbb7..b06946ad25 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -86,7 +86,7 @@ (click)="redirectToResourcePolicyEditPage(entry.policy)"> - diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts index 7b5c020f1b..fb49fe084a 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts @@ -63,7 +63,7 @@ describe('DSOSelectorModalWrapperComponent', () => { }); it('should initially set the DSO to the activated route\'s item/collection/community', () => { - component.dsoRD$ + component.dsoRD .pipe(first()) .subscribe((a) => { expect(a).toEqual(itemRD); diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts index 881476cac6..c13bd2b69d 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts @@ -1,11 +1,12 @@ import { Injectable, Input, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { RemoteData } from '../../../core/data/remote-data'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { map } from 'rxjs/operators'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; +import { hasValue, isNotEmpty } from '../../empty.util'; export enum SelectorActionType { CREATE = 'create', @@ -21,7 +22,7 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { /** * The current page's DSO */ - @Input() dsoRD$: Observable>; + @Input() dsoRD: RemoteData; /** * The type of the DSO that's being edited or created @@ -45,10 +46,30 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { * Get de current page's DSO based on the selectorType */ ngOnInit(): void { - const typeString = this.selectorType.toString().toLowerCase(); - this.dsoRD$ = this.route.root.firstChild.firstChild.data.pipe(map((data) => data[typeString])); + const matchingRoute = this.findRouteData( + (route: ActivatedRouteSnapshot) => hasValue(route.data.dso), + this.route.root.snapshot + ); + if (hasValue(matchingRoute)) { + this.dsoRD = matchingRoute.data.dso; + } } + findRouteData(predicate: (value: ActivatedRouteSnapshot, index?: number, obj?: ActivatedRouteSnapshot[]) => unknown, ...routes: ActivatedRouteSnapshot[]) { + const result = routes.find(predicate); + if (hasValue(result)) { + return result; + } else { + const nextLevelRoutes = routes + .map((route: ActivatedRouteSnapshot) => route.children) + .reduce((combined: ActivatedRouteSnapshot[], current: ActivatedRouteSnapshot[]) => [...combined, ...current]); + if (isNotEmpty(nextLevelRoutes)) { + return this.findRouteData(predicate, ...nextLevelRoutes) + } else { + return undefined; + } + } + } /** * Method called when an object has been selected * @param dso The selected DSpaceObject From 405815ac5a60ccf047f02db5a44ed1dd193e7ad2 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Mon, 20 Apr 2020 13:36:01 +0200 Subject: [PATCH 097/103] fix test --- .../dso-selector-modal-wrapper.component.spec.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts index fb49fe084a..c51edc5d9c 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts @@ -63,11 +63,7 @@ describe('DSOSelectorModalWrapperComponent', () => { }); it('should initially set the DSO to the activated route\'s item/collection/community', () => { - component.dsoRD - .pipe(first()) - .subscribe((a) => { - expect(a).toEqual(itemRD); - }) + expect(component.dsoRD).toEqual(itemRD); }); describe('selectObject', () => { From 6c1e636f5c257fe83b84e8018a8e442641aa2c82 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 27 May 2020 14:15:46 +0200 Subject: [PATCH 098/103] fix tests --- ...eate-collection-parent-selector.component.spec.ts | 10 +++++++++- ...reate-community-parent-selector.component.spec.ts | 10 +++++++++- .../create-item-parent-selector.component.spec.ts | 10 +++++++++- .../dso-selector-modal-wrapper.component.spec.ts | 12 ++++++++++-- .../edit-collection-selector.component.spec.ts | 10 +++++++++- .../edit-community-selector.component.spec.ts | 10 +++++++++- .../edit-item-selector.component.spec.ts | 10 +++++++++- 7 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts index 480f6ff709..df62534593 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts @@ -39,7 +39,15 @@ describe('CreateCollectionParentSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: communityRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts index b723d3fe98..9c6185199c 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts @@ -33,7 +33,15 @@ describe('CreateCommunityParentSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: communityRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts index 854349a47c..e8cd35fb50 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts @@ -32,7 +32,15 @@ describe('CreateItemParentSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: collectionRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts index c51edc5d9c..f52ce3fc8f 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts @@ -41,8 +41,16 @@ describe('DSOSelectorModalWrapperComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } } - } + useValue: { + root: { + snapshot: { + data: { + dso: itemRD, + }, + }, + } + } + }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts index a17d9e4c21..21ff5e846d 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts @@ -33,7 +33,15 @@ describe('EditCollectionSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: collectionRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts index c48d29baa9..b37fa23024 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts @@ -33,7 +33,15 @@ describe('EditCommunitySelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: communityRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts index 582320acae..e310d6ac02 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts @@ -33,7 +33,15 @@ describe('EditItemSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: itemRD, + }, + }, + } + }, }, { provide: Router, useValue: router From 6bd04c94ec0aab592896ca88ed91cfc95a5ca9a6 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 11 Jun 2020 14:15:19 +0200 Subject: [PATCH 099/103] store token on AUTHENTICATED_SUCCESS action --- src/app/core/auth/auth.effects.spec.ts | 9 ++++++++- src/app/core/auth/auth.effects.ts | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index c08615ecc9..98210b7b27 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -150,7 +150,8 @@ describe('AuthEffects', () => { describe('authenticatedSuccess$', () => { - it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', () => { + it('should not call removeToken method', (done) => { + spyOn((authEffects as any).authService, 'storeToken'); actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: { @@ -163,8 +164,14 @@ describe('AuthEffects', () => { const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) }); + authEffects.authenticatedSuccess$.subscribe(() => { + expect(authServiceStub.storeToken).toHaveBeenCalledWith(token); + }); + expect(authEffects.authenticatedSuccess$).toBeObservable(expected); + done(); }); + }); describe('checkToken$', () => { diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index c6d447961a..5591ffbe39 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -65,7 +65,6 @@ export class AuthEffects { @Effect() public authenticateSuccess$: Observable = this.actions$.pipe( ofType(AuthActionTypes.AUTHENTICATE_SUCCESS), - tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)), map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)) ); @@ -82,6 +81,7 @@ export class AuthEffects { @Effect() public authenticatedSuccess$: Observable = this.actions$.pipe( ofType(AuthActionTypes.AUTHENTICATED_SUCCESS), + tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)), map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref)) ); From c835d9b9c71bf95e5134cb404e98cdafe069b6a9 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 11 Jun 2020 14:44:42 +0200 Subject: [PATCH 100/103] fixed test description --- src/app/core/auth/auth.effects.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 98210b7b27..79fe385c6d 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -150,7 +150,7 @@ describe('AuthEffects', () => { describe('authenticatedSuccess$', () => { - it('should not call removeToken method', (done) => { + it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', (done) => { spyOn((authEffects as any).authService, 'storeToken'); actions = hot('--a-', { a: { From 7421eef223c21a0f5f8f1bb984ca81cfd580986a Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 12 Jun 2020 09:54:13 +0200 Subject: [PATCH 101/103] Misc edit community and collection bugs --- .../comcol-form/comcol-form.component.spec.ts | 66 +++++++++++-------- .../comcol-form/comcol-form.component.ts | 32 +++++++-- .../comcol-metadata.component.spec.ts | 31 ++++++--- .../comcol-metadata.component.ts | 42 +++++++----- src/assets/i18n/en.json5 | 4 ++ 5 files changed, 115 insertions(+), 60 deletions(-) diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts index a1bac46f87..3fcdc280d0 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -22,6 +22,7 @@ import { NotificationsService } from '../../notifications/notifications.service' import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { VarDirective } from '../../utils/var.directive'; import { ComColFormComponent } from './comcol-form.component'; +import { Operation } from 'fast-json-patch'; describe('ComColFormComponent', () => { let comp: ComColFormComponent; @@ -40,11 +41,8 @@ describe('ComColFormComponent', () => { } }; const dcTitle = 'dc.title'; - const dcRandom = 'dc.random'; const dcAbstract = 'dc.description.abstract'; - const titleMD = { [dcTitle]: [{ value: 'Community Title', language: null }] }; - const randomMD = { [dcRandom]: [{ value: 'Random metadata excluded from form', language: null }] }; const abstractMD = { [dcAbstract]: [{ value: 'Community description', language: null }] }; const newTitleMD = { [dcTitle]: [{ value: 'New Community Title', language: null }] }; const formModel = [ @@ -112,33 +110,47 @@ describe('ComColFormComponent', () => { }); it('should emit the new version of the community', () => { - comp.dso = Object.assign( - new Community(), - { - metadata: { - ...titleMD, - ...randomMD - } - } - ); + comp.dso = new Community(); comp.onSubmit(); + const operations: Operation[] = [ + { + op: 'replace', + path: '/metadata/dc.title', + value: { + value: 'New Community Title', + language: null, + }, + }, + { + op: 'replace', + path: '/metadata/dc.description.abstract', + value: { + value: 'Community description', + language: null, + }, + }, + ]; + expect(comp.submitForm.emit).toHaveBeenCalledWith( { - dso: Object.assign( - {}, - new Community(), - { + dso: Object.assign({}, comp.dso, { metadata: { - ...newTitleMD, - ...randomMD, - ...abstractMD + 'dc.title': [{ + value: 'New Community Title', + language: null, + }], + 'dc.description.abstract': [{ + value: 'Community description', + language: null, + }], }, - type: Community.type - }, + type: Community.type, + } ), uploader: undefined, - deleteLogo: false + deleteLogo: false, + operations: operations, } ); }) @@ -164,11 +176,6 @@ describe('ComColFormComponent', () => { it('should emit finish', () => { expect(comp.finish.emit).toHaveBeenCalled(); }); - - it('should remove the object\'s cache', () => { - expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled(); - expect(objectCacheStub.remove).toHaveBeenCalled(); - }); }); describe('onUploadError', () => { @@ -239,6 +246,11 @@ describe('ComColFormComponent', () => { it('should display a success notification', () => { expect(notificationsService.success).toHaveBeenCalled(); }); + + it('should remove the object\'s cache', () => { + expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled(); + expect(objectCacheStub.remove).toHaveBeenCalled(); + }); }); describe('when dsoService.deleteLogo returns an error response', () => { diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index f8199d2aad..91e896ce6c 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -25,6 +25,7 @@ import { hasValue, isNotEmpty } from '../../empty.util'; import { NotificationsService } from '../../notifications/notifications.service'; import { UploaderOptions } from '../../uploader/uploader-options.model'; import { UploaderComponent } from '../../uploader/uploader.component'; +import { Operation } from 'fast-json-patch'; /** * A form for creating and editing Communities or Collections @@ -85,7 +86,8 @@ export class ComColFormComponent implements OnInit, OnDe @Output() submitForm: EventEmitter<{ dso: T, uploader: FileUploader, - deleteLogo: boolean + deleteLogo: boolean, + operations: Operation[], }> = new EventEmitter(); /** @@ -189,9 +191,9 @@ export class ComColFormComponent implements OnInit, OnDe const formMetadata = {} as MetadataMap; this.formModel.forEach((fieldModel: DynamicInputModel) => { const value: MetadataValue = { - value: fieldModel.value as string, - language: null - } as any; + value: fieldModel.value as string, + language: null + } as any; if (formMetadata.hasOwnProperty(fieldModel.name)) { formMetadata[fieldModel.name].push(value); } else { @@ -206,10 +208,26 @@ export class ComColFormComponent implements OnInit, OnDe }, type: Community.type }); + + const operations: Operation[] = []; + this.formModel.forEach((fieldModel: DynamicInputModel) => { + if (fieldModel.value !== this.dso.firstMetadataValue(fieldModel.name)) { + operations.push({ + op: 'replace', + path: `/metadata/${fieldModel.name}`, + value: { + value: fieldModel.value, + language: null, + }, + }); + } + }); + this.submitForm.emit({ dso: updatedDSO, uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined, - deleteLogo: this.markLogoForDeletion + deleteLogo: this.markLogoForDeletion, + operations: operations, }); } @@ -257,7 +275,9 @@ export class ComColFormComponent implements OnInit, OnDe * The request was successful, display a success notification */ public onCompleteItem() { - this.refreshCache(); + if (hasValue(this.dso.id)) { + this.refreshCache(); + } this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success')); this.finish.emit(); } diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts index 414d64cbff..c606f50a71 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts @@ -27,6 +27,7 @@ describe('ComColMetadataComponent', () => { let communityDataServiceStub; let routerStub; let routeStub; + let isSuccessful = true; const logoEndpoint = 'rest/api/logo/endpoint'; @@ -49,6 +50,11 @@ describe('ComColMetadataComponent', () => { communityDataServiceStub = { update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), + patch: () => { + return observableOf({ + isSuccessful, + }) + }, getLogoEndpoint: () => observableOf(logoEndpoint) }; @@ -95,21 +101,28 @@ describe('ComColMetadataComponent', () => { describe('with an empty queue in the uploader', () => { beforeEach(() => { data = { - dso: Object.assign(new Community(), { - metadata: [{ - key: 'dc.title', - value: 'test' - }] - }), + operations: [ + { + op: 'replace', + path: '/metadata/dc.title', + value: { + value: 'test', + language: null, + }, + }, + ], + dso: new Community(), uploader: { options: { url: '' }, queue: [], /* tslint:disable:no-empty */ - uploadAll: () => {} + uploadAll: () => { + } /* tslint:enable:no-empty */ - } + }, + deleteLogo: false, } }); @@ -121,8 +134,8 @@ describe('ComColMetadataComponent', () => { }); it('should not navigate on failure', () => { + isSuccessful = false; spyOn(router, 'navigate'); - spyOn(dsoDataService, 'update').and.returnValue(createFailedRemoteDataObject$(newCommunity)); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts index 1031fead10..02c28d989c 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts @@ -5,8 +5,7 @@ import { RemoteData } from '../../../../core/data/remote-data'; import { ActivatedRoute, Router } from '@angular/router'; import { first, map, take } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../../../core/shared/operators'; -import { hasValue, isNotUndefined } from '../../../empty.util'; -import { DataService } from '../../../../core/data/data.service'; +import { hasValue, isEmpty } from '../../../empty.util'; import { ResourceType } from '../../../../core/shared/resource-type'; import { ComColDataService } from '../../../../core/data/comcol-data.service'; import { NotificationsService } from '../../../notifications/notifications.service'; @@ -49,26 +48,33 @@ export class ComcolMetadataComponent implements On * @param event The event returned by the community/collection form. Contains the new dso and logo uploader */ onSubmit(event) { - const dso = event.dso; + const uploader = event.uploader; const deleteLogo = event.deleteLogo; - this.dsoDataService.update(dso) - .pipe(getSucceededRemoteData()) - .subscribe((dsoRD: RemoteData) => { - if (isNotUndefined(dsoRD)) { - const newUUID = dsoRD.payload.uuid; - if (hasValue(uploader) && uploader.queue.length > 0) { - this.dsoDataService.getLogoEndpoint(newUUID).pipe(take(1)).subscribe((href: string) => { - uploader.options.url = href; - uploader.uploadAll(); - }); - } else if (!deleteLogo) { - this.router.navigate([this.frontendURL + newUUID]); - } - this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.notifications.success')); - } + const newLogo = hasValue(uploader) && uploader.queue.length > 0; + if (newLogo) { + this.dsoDataService.getLogoEndpoint(event.dso.uuid).pipe(take(1)).subscribe((href: string) => { + uploader.options.url = href; + uploader.uploadAll(); }); + } + + if (!isEmpty(event.operations)) { + this.dsoDataService.patch(event.dso, event.operations) + .subscribe(async (response) => { + if (response.isSuccessful) { + if (!newLogo && !deleteLogo) { + await this.router.navigate([this.frontendURL + event.dso.uuid]); + } + this.notificationsService.success(null, this.translate.get(`${this.type.value}.edit.notifications.success`)); + } else if (response.statusCode === 403) { + this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.unauthorized`)); + } else { + this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.error`)); + } + }); + } } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4173fa1cf2..aad6cbf7b1 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -767,6 +767,10 @@ "community.edit.notifications.success": "Successfully edited the Community", + "community.edit.notifications.unauthorized": "You do not have privileges to make this change", + + "community.edit.notifications.error": "An error occured while editing the Community", + "community.edit.return": "Return", From b8ab83ce998ed95c3e36a618d5e5bd4a9d1b9edc Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 12 Jun 2020 11:11:31 +0200 Subject: [PATCH 102/103] Misc edit community and collection bugs - repair test --- .../comcol-metadata.component.spec.ts | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts index c606f50a71..b50a1d3ac4 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts @@ -13,13 +13,13 @@ import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NotificationsService } from '../../../notifications/notifications.service'; import { SharedModule } from '../../../shared.module'; import { NotificationsServiceStub } from '../../../testing/notifications-service.stub'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { ComcolMetadataComponent } from './comcol-metadata.component'; describe('ComColMetadataComponent', () => { let comp: ComcolMetadataComponent; let fixture: ComponentFixture>; - let dsoDataService: CommunityDataService; + let dsoDataService; let router: Router; let community; @@ -27,7 +27,6 @@ describe('ComColMetadataComponent', () => { let communityDataServiceStub; let routerStub; let routeStub; - let isSuccessful = true; const logoEndpoint = 'rest/api/logo/endpoint'; @@ -50,11 +49,7 @@ describe('ComColMetadataComponent', () => { communityDataServiceStub = { update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), - patch: () => { - return observableOf({ - isSuccessful, - }) - }, + patch: () => null, getLogoEndpoint: () => observableOf(logoEndpoint) }; @@ -123,22 +118,38 @@ describe('ComColMetadataComponent', () => { /* tslint:enable:no-empty */ }, deleteLogo: false, - } + }; + spyOn(router, 'navigate'); }); - it('should navigate when successful', () => { - spyOn(router, 'navigate'); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).toHaveBeenCalled(); + describe('when successful', () => { + + beforeEach(() => { + spyOn(dsoDataService, 'patch').and.returnValue(observableOf({ + isSuccessful: true, + })); + }); + + it('should navigate', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).toHaveBeenCalled(); + }); }); - it('should not navigate on failure', () => { - isSuccessful = false; - spyOn(router, 'navigate'); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).not.toHaveBeenCalled(); + describe('on failure', () => { + + beforeEach(() => { + spyOn(dsoDataService, 'patch').and.returnValue(observableOf({ + isSuccessful: false, + })); + }); + + it('should not navigate', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); }); }); From 0721b844f5f18528ba39a8865a347ea6c3542fc0 Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 18 Jun 2020 12:25:50 +0200 Subject: [PATCH 103/103] Misc edit community and collection bugs - repair create top level community --- .../create-comcol-page/create-comcol-page.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index a8d6499cbd..4a7cd9afb1 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -77,7 +77,8 @@ export class CreateComColPageComponent implements const uploader = event.uploader; this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - this.dsoDataService.create(dso, new RequestParam('parent', uuid)) + const params = uuid ? [new RequestParam('parent', uuid)] : []; + this.dsoDataService.create(dso, ...params) .pipe(getSucceededRemoteData()) .subscribe((dsoRD: RemoteData) => { if (isNotUndefined(dsoRD)) {
{{getGroupName(entry.policy) | async}} - {{formatDate(entry.policy.startDate)}} {{formatDate(entry.policy.endDate)}} - + +
+ + +