diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 835a7f6ba7..5aa462d5e0 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -160,6 +160,8 @@ import { SubmissionCcLicenseDataService } from './submission/submission-cc-licen import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model'; import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service'; +import { ConfigurationDataService } from './data/configuration-data.service'; +import { ConfigurationProperty } from './shared/configuration-property.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -245,6 +247,7 @@ const PROVIDERS = [ UploaderService, FileService, DSpaceObjectDataService, + ConfigurationDataService, DSOChangeAnalyzer, DefaultChangeAnalyzer, ArrayMoveChangeAnalyzer, @@ -350,7 +353,8 @@ export const models = TemplateItem, Feature, Authorization, - Registration + Registration, + ConfigurationProperty ]; @NgModule({ diff --git a/src/app/core/data/configuration-data.service.spec.ts b/src/app/core/data/configuration-data.service.spec.ts new file mode 100644 index 0000000000..fde55070e1 --- /dev/null +++ b/src/app/core/data/configuration-data.service.spec.ts @@ -0,0 +1,87 @@ +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { FindByIDRequest } from './request.models'; +import { RequestService } from './request.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { ConfigurationDataService } from './configuration-data.service'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; + +describe('ConfigurationDataService', () => { + let scheduler: TestScheduler; + let service: ConfigurationDataService; + let halService: HALEndpointService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + const testObject = { + uuid: 'test-property', + name: 'test-property', + values: ['value-1', 'value-2'] + } as ConfigurationProperty; + const configLink = 'https://rest.api/rest/api/config/properties'; + const requestURL = `https://rest.api/rest/api/config/properties/${testObject.name}`; + const requestUUID = 'test-property'; + + beforeEach(() => { + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', {a: configLink}) + }); + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: cold('a', { + a: { + payload: testObject + } + }) + }); + objectCache = {} as ObjectCacheService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + + service = new ConfigurationDataService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + http, + comparator + ); + }); + + describe('findById', () => { + it('should call HALEndpointService with the path to the properties endpoint', () => { + scheduler.schedule(() => service.findByPropertyName(testObject.name)); + scheduler.flush(); + + expect(halService.getEndpoint).toHaveBeenCalledWith('properties'); + }); + + it('should configure the proper FindByIDRequest', () => { + scheduler.schedule(() => service.findByPropertyName(testObject.name)); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.name)); + }); + + it('should return a RemoteData for the object with the given name', () => { + const result = service.findByPropertyName(testObject.name); + const expected = cold('a', { + a: { + payload: testObject + } + }); + expect(result).toBeObservable(expected); + }); + }); +}); diff --git a/src/app/core/data/configuration-data.service.ts b/src/app/core/data/configuration-data.service.ts new file mode 100644 index 0000000000..8e38c97ca1 --- /dev/null +++ b/src/app/core/data/configuration-data.service.ts @@ -0,0 +1,66 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +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 { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { DSPACE_OBJECT } from '../shared/dspace-object.resource-type'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { DataService } from './data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { CONFIG_PROPERTY } from '../shared/config-property.resource-type'; + +/* tslint:disable:max-classes-per-file */ +class DataServiceImpl extends DataService { + protected linkPath = 'properties'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } +} + +@Injectable() +@dataService(CONFIG_PROPERTY) +/** + * Data Service responsible for retrieving Configuration properties + */ +export class ConfigurationDataService { + protected linkPath = 'properties'; + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Finds a configuration property by name + * @param name + */ + findByPropertyName(name: string): Observable> { + return this.dataService.findById(name); + } +} diff --git a/src/app/core/shared/config-property.resource-type.ts b/src/app/core/shared/config-property.resource-type.ts new file mode 100644 index 0000000000..b93c29dd66 --- /dev/null +++ b/src/app/core/shared/config-property.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for ConfigurationProperty + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const CONFIG_PROPERTY = new ResourceType('property'); diff --git a/src/app/core/shared/configuration-property.model.ts b/src/app/core/shared/configuration-property.model.ts new file mode 100644 index 0000000000..465523c29f --- /dev/null +++ b/src/app/core/shared/configuration-property.model.ts @@ -0,0 +1,48 @@ +import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; +import { typedObject } from '../cache/builders/build-decorators'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { HALLink } from './hal-link.model'; +import { ResourceType } from './resource-type'; +import { CONFIG_PROPERTY } from './config-property.resource-type'; + +/** + * Model class for a Configuration Property + */ +@typedObject +export class ConfigurationProperty implements CacheableObject { + static type = CONFIG_PROPERTY; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The uuid of the configuration property + * The name is used as id for configuration properties + */ + @autoserializeAs(String, 'name') + uuid: string; + + /** + * The name of the configuration property + */ + @autoserialize + name: string; + + /** + * The values of the configuration property + */ + @autoserialize + values: string[]; + + /** + * The links of the configuration property + */ + @deserialize + _links: { self: HALLink }; + +} diff --git a/src/app/curation-form/curation-form.component.html b/src/app/curation-form/curation-form.component.html index 129a51a037..c940494016 100644 --- a/src/app/curation-form/curation-form.component.html +++ b/src/app/curation-form/curation-form.component.html @@ -4,8 +4,8 @@
diff --git a/src/app/curation-form/curation-form.component.spec.ts b/src/app/curation-form/curation-form.component.spec.ts index 2fada44742..76aaa7fbf1 100644 --- a/src/app/curation-form/curation-form.component.spec.ts +++ b/src/app/curation-form/curation-form.component.spec.ts @@ -17,6 +17,8 @@ import { NotificationsService } from '../shared/notifications/notifications.serv import { Router } from '@angular/router'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; describe('CurationFormComponent', () => { let comp: CurationFormComponent; @@ -24,6 +26,7 @@ describe('CurationFormComponent', () => { let scriptDataService: ScriptDataService; let processDataService: ProcessDataService; + let configurationDataService: ConfigurationDataService; let authService: AuthService; let notificationsService; let router; @@ -49,6 +52,17 @@ describe('CurationFormComponent', () => { getAuthenticatedUserFromStore: observableOf(Object.assign(new EPerson(), {email: 'test@mail'})) }); + configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'plugin.named.org.dspace.curate.CurationTask', + values: [ + 'org.dspace.ctask.general.ProfileFormats = profileformats', + 'org.dspace.ctask.general.RequiredMetadata = requiredmetadata', + 'org.dspace.ctask.general.MetadataValueLinkChecker = checklinks' + ] + })) + }); + notificationsService = new NotificationsServiceStub(); router = new RouterStub(); @@ -61,6 +75,7 @@ describe('CurationFormComponent', () => { {provide: AuthService, useValue: authService}, {provide: NotificationsService, useValue: notificationsService}, {provide: Router, useValue: router}, + {provide: ConfigurationDataService, useValue: configurationDataService}, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); diff --git a/src/app/curation-form/curation-form.component.ts b/src/app/curation-form/curation-form.component.ts index 6f2d9f0b74..e69e08f8a9 100644 --- a/src/app/curation-form/curation-form.component.ts +++ b/src/app/curation-form/curation-form.component.ts @@ -1,12 +1,10 @@ import { Component, Input, OnInit } from '@angular/core'; import { ScriptDataService } from '../core/data/processes/script-data.service'; -import { environment } from '../../environments/environment'; -import { CurationTask } from '../../config/curation-task.interface'; import { FormControl, FormGroup } from '@angular/forms'; import { getResponseFromEntry } from '../core/shared/operators'; import { DSOSuccessResponse } from '../core/cache/response.models'; import { AuthService } from '../core/auth/auth.service'; -import { filter, switchMap, take } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { EPerson } from '../core/eperson/models/eperson.model'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; @@ -15,6 +13,12 @@ import { RemoteData } from '../core/data/remote-data'; import { Router } from '@angular/router'; import { ProcessDataService } from '../core/data/processes/process-data.service'; import { Process } from '../process-page/processes/process.model'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; +import { Observable } from 'rxjs'; +import { find } from 'rxjs/internal/operators/find'; + +export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask'; /** * Component responsible for rendering the Curation Task form @@ -25,7 +29,8 @@ import { Process } from '../process-page/processes/process.model'; }) export class CurationFormComponent implements OnInit { - tasks: CurationTask[]; + config: Observable>; + tasks: string[]; form: FormGroup; @Input() @@ -33,6 +38,7 @@ export class CurationFormComponent implements OnInit { constructor( private scriptDataService: ScriptDataService, + private configurationDataService: ConfigurationDataService, private processDataService: ProcessDataService, private authService: AuthService, private notificationsService: NotificationsService, @@ -42,12 +48,19 @@ export class CurationFormComponent implements OnInit { } ngOnInit(): void { - this.tasks = environment.curationTasks; - this.form = new FormGroup({ - task: new FormControl(this.tasks[0]), + task: new FormControl(''), handle: new FormControl('') }); + + this.config = this.configurationDataService.findByPropertyName(CURATION_CFG); + this.config.pipe( + find((rd: RemoteData) => rd.hasSucceeded), + map((rd: RemoteData) => rd.payload) + ).subscribe((configProperties) => { + this.tasks = configProperties.values.map((value) => value.split('=')[1].trim()); + this.form.get('task').patchValue(this.tasks[0]); + }); } /** @@ -65,7 +78,7 @@ export class CurationFormComponent implements OnInit { * Navigate to the process page on success */ submit() { - const taskName = (this.form.get('task').value as CurationTask).name; + const taskName = this.form.get('task').value; let handle; if (this.hasHandleValue()) { handle = this.dsoHandle; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 03153142d6..12429fe3b0 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -917,11 +917,17 @@ + "curation-task.task.checklinks.label": "Check Links in Metadata", + + "curation-task.task.noop.label": "NOOP", + "curation-task.task.profileformats.label": "Profile Bitstream Formats", "curation-task.task.requiredmetadata.label": "Check for Required Metadata", - "curation-task.task.checklinks.label": "Check Links in Metadata", + "curation-task.task.translate.label": "Microsoft Translator", + + "curation-task.task.vscan.label": "Virus Scan", diff --git a/src/config/curation-task.interface.ts b/src/config/curation-task.interface.ts deleted file mode 100644 index bcdb595756..0000000000 --- a/src/config/curation-task.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Config } from './config.interface'; - -/** - * An interface to represent a curation task in the configuration. A CurationTask has a name and a label. - */ -export interface CurationTask extends Config { - name: string; - label: string; -} diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts index 56f5f7e3c8..acef3404eb 100644 --- a/src/config/global-config.interface.ts +++ b/src/config/global-config.interface.ts @@ -11,7 +11,6 @@ import { ItemPageConfig } from './item-page-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface'; import { Theme } from './theme.inferface'; import {AuthConfig} from './auth-config.interfaces'; -import { CurationTask } from './curation-task.interface'; export interface GlobalConfig extends Config { ui: ServerConfig; @@ -32,5 +31,4 @@ export interface GlobalConfig extends Config { item: ItemPageConfig; collection: CollectionPageConfig; theme: Theme; - curationTasks: CurationTask[]; } diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index bdd7e3c738..c174d9078d 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -213,18 +213,4 @@ export const environment: GlobalConfig = { theme: { name: 'default', }, - curationTasks: [ - { - name: 'profileformats', - label: 'curation-task.task.profileformats.label' - }, - { - name: 'requiredmetadata', - label: 'curation-task.task.requiredmetadata.label' - }, - { - name: 'checklinks', - label: 'curation-task.task.checklinks.label' - } - ] }; diff --git a/src/environments/mock-environment.ts b/src/environments/mock-environment.ts index ad72277ed8..6e4d60e268 100644 --- a/src/environments/mock-environment.ts +++ b/src/environments/mock-environment.ts @@ -196,18 +196,4 @@ export const environment: Partial = { theme: { name: 'default', }, - curationTasks: [ - { - name: 'profileformats', - label: 'curation-task.task.profileformats.label' - }, - { - name: 'requiredmetadata', - label: 'curation-task.task.requiredmetadata.label' - }, - { - name: 'checklinks', - label: 'curation-task.task.checklinks.label' - } - ] };