From e396fe054e66518d0587f9814a38682c11b4bde8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 26 Mar 2020 14:40:06 +0100 Subject: [PATCH 01/46] 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 02/46] 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 03/46] 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 04/46] 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 05/46] 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 06/46] 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 07/46] 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 08/46] 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 09/46] 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 b603c7fc0556c45c0901591d5c07faa2699d3086 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Apr 2020 18:52:53 +0200 Subject: [PATCH 10/46] 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 11/46] 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 12/46] 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 13/46] 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 14/46] 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 15/46] 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 16/46] 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 17/46] 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 6314e17f274b20d9131d8e5ac9c9bfc0d1d82be7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 3 Apr 2020 20:25:58 +0200 Subject: [PATCH 18/46] 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 19/46] 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 4c391bbae4e3c8e6970c60566f6ea9b08e0f559e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 6 Apr 2020 20:11:20 +0200 Subject: [PATCH 20/46] 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 21/46] 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 22/46] 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 23/46] 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 24/46] 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 25/46] 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 46498992a729aa1772a3772c0e25251ed3b0fd80 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 14 Apr 2020 20:58:58 +0200 Subject: [PATCH 26/46] 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 27/46] 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 e7d0c96a2666b9c37306e0e1538d709ddbc1a2b2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:11:24 +0200 Subject: [PATCH 28/46] 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 29/46] 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 30/46] 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 31/46] 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 32/46] 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 33/46] 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 98ade2eb630dd162c732b39fe6f46705ac817912 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:35:29 +0200 Subject: [PATCH 34/46] 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 c03146e415dc808903906e9bc0ccd0959dad0bda Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 16 Apr 2020 17:12:20 +0200 Subject: [PATCH 35/46] 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 b338332d13ca1ecef18ced09d35b821796b468e6 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Apr 2020 15:43:52 +0200 Subject: [PATCH 36/46] 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 37/46] 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 b70f8e12f6478d524939300afc176c6d5ddbdc86 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 20 May 2020 17:05:54 +0200 Subject: [PATCH 38/46] 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 39/46] 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 40/46] 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 41/46] 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 42/46] 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 43/46] 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 44/46] 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 f0bf87b7f92bcc63910124a18a5c14a978da7a39 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 28 May 2020 11:39:42 +0200 Subject: [PATCH 45/46] 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)"> -
{{getGroupName(entry.policy) | async}} - {{formatDate(entry.policy.startDate)}} {{formatDate(entry.policy.endDate)}} - + +
+ + +