diff --git a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts
index f2687e452c..bbcb7f65bf 100644
--- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts
+++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts
@@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
-import { async, ComponentFixture, inject, TestBed, tick, fakeAsync } from '@angular/core/testing';
+import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Store } from '@ngrx/store';
@@ -15,7 +15,6 @@ import { createTestComponent } from '../../shared/testing/utils.test';
import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission.component';
import { AppState } from '../../app.reducer';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
-import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { SharedModule } from '../../shared/shared.module';
@@ -25,10 +24,25 @@ import { By } from '@angular/platform-browser';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
+import { UploaderComponent } from 'src/app/shared/uploader/uploader.component';
-describe('MyDSpaceNewSubmissionComponent test', () => {
+fdescribe('MyDSpaceNewSubmissionComponent test', () => {
+
+ const translateService: TranslateService = jasmine.createSpyObj('translateService', {
+ get: (key: string): any => { observableOf(key) },
+ instant: jasmine.createSpy('instant')
+ });
+
+ const uploader: any = jasmine.createSpyObj('uploader', {
+ clearQueue: jasmine.createSpy('clearQueue')
+ });
+
+ const modalService = {
+ open: () => {
+ return { result: new Promise((res, rej) => {/****/}) };
+ }
+ };
- const translateService: any = getMockTranslateService();
const store: Store
= jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */
dispatch: {},
@@ -58,11 +72,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
{ provide: ScrollToService, useValue: getMockScrollToService() },
{ provide: Store, useValue: store },
{ provide: TranslateService, useValue: translateService },
- {
- provide: NgbModal, useValue: {
- open: () => {/*comment*/}
- }
- },
+ { provide: NgbModal, useValue: modalService },
ChangeDetectorRef,
MyDSpaceNewSubmissionComponent,
UploaderService,
@@ -103,6 +113,10 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
beforeEach(() => {
fixture = TestBed.createComponent(MyDSpaceNewSubmissionComponent);
comp = fixture.componentInstance;
+ comp.uploadFilesOptions.authToken = 'user-auth-token';
+ comp.uploadFilesOptions.url = 'https://fake.upload-api.url';
+ comp.uploaderComponent = TestBed.createComponent(UploaderComponent).componentInstance;
+ comp.uploaderComponent.uploader = uploader;
});
it('should call app.openDialog', () => {
@@ -114,6 +128,12 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
});
expect(comp.openDialog).toHaveBeenCalled();
});
+
+ it('should show a collection selector if only one file are uploaded', () => {
+ spyOn((comp as any).modalService, 'open').and.returnValue({ result: new Promise((res, rej) => {/****/}) });
+ comp.afterFileLoaded(['']);
+ expect((comp as any).modalService.open).toHaveBeenCalled();
+ });
});
});
diff --git a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts
index 5659c442ad..d14fe46afd 100644
--- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts
+++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts
@@ -1,13 +1,10 @@
-import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
-import { Router } from '@angular/router';
+import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { first } from 'rxjs/operators';
-import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
-import { SubmissionState } from '../../submission/submission.reducers';
import { AuthService } from '../../core/auth/auth.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -18,6 +15,9 @@ import { NotificationType } from '../../shared/notifications/models/notification
import { hasValue } from '../../shared/empty.util';
import { SearchResult } from '../../shared/search/search-result.model';
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
+import { CollectionSelectorComponent } from '../collection-selector/collection-selector.component';
+import { UploaderComponent } from '../../shared/uploader/uploader.component';
+import { UploaderError } from '../../shared/uploader/uploader-error.model';
/**
* This component represents the whole mydspace page header
@@ -43,6 +43,11 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
*/
private sub: Subscription;
+ /**
+ * Reference to uploaderComponent
+ */
+ @ViewChild(UploaderComponent, { static: false }) uploaderComponent: UploaderComponent;
+
/**
* Initialize instance variables
*
@@ -59,9 +64,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private halService: HALEndpointService,
private notificationsService: NotificationsService,
- private store: Store,
private translate: TranslateService,
- private router: Router,
private modalService: NgbModal) {
}
@@ -69,6 +72,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
* Initialize url and Bearer token
*/
ngOnInit() {
+ this.uploadFilesOptions.autoUpload = false;
this.sub = this.halService.getEndpoint('workspaceitems').pipe(first()).subscribe((url) => {
this.uploadFilesOptions.url = url;
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
@@ -108,8 +112,12 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
/**
* Method called on file upload error
*/
- public onUploadError() {
- this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
+ public onUploadError(error: UploaderError) {
+ let errorMessageKey = 'mydspace.upload.upload-failed';
+ if (hasValue(error.status) && error.status === 422) {
+ errorMessageKey = 'mydspace.upload.upload-failed-manyentries';
+ }
+ this.notificationsService.error(null, this.translate.get(errorMessageKey));
}
/**
@@ -120,6 +128,28 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
this.modalService.open(CreateItemParentSelectorComponent);
}
+ /**
+ * Method invoked after all file are loaded from upload plugin
+ */
+ afterFileLoaded(items) {
+ const uploader = this.uploaderComponent.uploader;
+ if (hasValue(items) && items.length > 1) {
+ this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed-moreonefile'));
+ uploader.clearQueue();
+ this.changeDetectorRef.detectChanges();
+ } else {
+ const modalRef = this.modalService.open(CollectionSelectorComponent);
+ // When the dialog are closes its takes the collection selected and
+ // uploads choosed file after adds owningCollection parameter
+ modalRef.result.then( (result) => {
+ uploader.onBuildItemForm = (fileItem: any, form: any) => {
+ form.append('owningCollection', result.uuid);
+ };
+ uploader.uploadAll();
+ });
+ }
+ }
+
/**
* Unsubscribe from the subscription
*/
diff --git a/src/app/+my-dspace-page/my-dspace-page.module.ts b/src/app/+my-dspace-page/my-dspace-page.module.ts
index 1cf30c4ec9..49570fec6d 100644
--- a/src/app/+my-dspace-page/my-dspace-page.module.ts
+++ b/src/app/+my-dspace-page/my-dspace-page.module.ts
@@ -20,6 +20,7 @@ import { SearchResultListElementComponent } from '../shared/object-list/search-r
import { ItemSearchResultListElementSubmissionComponent } from '../shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component';
import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component';
import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component';
+import { CollectionSelectorComponent } from './collection-selector/collection-selector.component';
@NgModule({
imports: [
@@ -40,7 +41,8 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/
ClaimedTaskSearchResultDetailElementComponent,
PoolSearchResultDetailElementComponent,
MyDSpaceNewSubmissionComponent,
- ItemSearchResultListElementSubmissionComponent
+ ItemSearchResultListElementSubmissionComponent,
+ CollectionSelectorComponent
],
providers: [
MyDSpaceGuard,
@@ -57,7 +59,8 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/
WorkflowItemSearchResultDetailElementComponent,
ClaimedTaskSearchResultDetailElementComponent,
PoolSearchResultDetailElementComponent,
- ItemSearchResultListElementSubmissionComponent
+ ItemSearchResultListElementSubmissionComponent,
+ CollectionSelectorComponent
]
})
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..ec8221ebec
--- /dev/null
+++ b/src/app/core/data/configuration-data.service.ts
@@ -0,0 +1,62 @@
+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 { 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 { HALEndpointService } from '../shared/hal-endpoint.service';
+import { DataService } from './data.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/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts
index a10b46c3d0..c186bc8dcd 100644
--- a/src/app/core/eperson/group-data.service.ts
+++ b/src/app/core/eperson/group-data.service.ts
@@ -40,7 +40,6 @@ import { GROUP } from './models/group.resource-type';
import { DSONameService } from '../breadcrumbs/dso-name.service';
import { Community } from '../shared/community.model';
import { Collection } from '../shared/collection.model';
-import { ComcolRole } from '../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role';
const groupRegistryStateSelector = (state: AppState) => state.groupRegistry;
const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup);
@@ -324,16 +323,17 @@ export class GroupDataService extends DataService {
* Create a group for a given role for a given community or collection.
*
* @param dso The community or collection for which to create a group
+ * @param role The name of the role for which to create a group
* @param link The REST endpoint to create the group
*/
- createComcolGroup(dso: Community|Collection, link: string): Observable {
+ createComcolGroup(dso: Community|Collection, role: string, link: string): Observable {
const requestId = this.requestService.generateRequestId();
const group = Object.assign(new Group(), {
metadata: {
'dc.description': [
{
- value: `${this.nameService.getName(dso)} admin group`,
+ value: `${this.nameService.getName(dso)} ${role} group`,
}
],
},
diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts
index b65ac252ef..c1464d7d39 100644
--- a/src/app/core/shared/collection.model.ts
+++ b/src/app/core/shared/collection.model.ts
@@ -15,8 +15,6 @@ import { RESOURCE_POLICY } from '../resource-policy/models/resource-policy.resou
import { COMMUNITY } from './community.resource-type';
import { Community } from './community.model';
import { ChildHALResource } from './child-hal-resource.model';
-import { GROUP } from '../eperson/models/group.resource-type';
-import { Group } from '../eperson/models/group.model';
@typedObject
@inheritSerialization(DSpaceObject)
@@ -41,6 +39,11 @@ export class Collection extends DSpaceObject implements ChildHALResource {
defaultAccessConditions: HALLink;
logo: HALLink;
parentCommunity: HALLink;
+ workflowGroups: HALLink[];
+ adminGroup: HALLink;
+ submittersGroup: HALLink;
+ itemReadGroup: HALLink;
+ bitstreamReadGroup: HALLink;
self: HALLink;
};
@@ -72,12 +75,6 @@ export class Collection extends DSpaceObject implements ChildHALResource {
@link(COMMUNITY, false)
parentCommunity?: Observable>;
- /**
- * The administrators group of this community.
- */
- @link(GROUP)
- adminGroup?: Observable>;
-
/**
* The introductory text of this Collection
* Corresponds to the metadata field dc.description
diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts
index bdcda70e9b..796aaa8ece 100644
--- a/src/app/core/shared/community.model.ts
+++ b/src/app/core/shared/community.model.ts
@@ -3,8 +3,6 @@ import { Observable } from 'rxjs';
import { link, typedObject } from '../cache/builders/build-decorators';
import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data';
-import { Group } from '../eperson/models/group.model';
-import { GROUP } from '../eperson/models/group.resource-type';
import { Bitstream } from './bitstream.model';
import { BITSTREAM } from './bitstream.resource-type';
import { Collection } from './collection.model';
@@ -66,12 +64,6 @@ export class Community extends DSpaceObject implements ChildHALResource {
@link(COMMUNITY, false)
parentCommunity?: Observable>;
- /**
- * The administrators group of this community.
- */
- @link(GROUP)
- adminGroup?: Observable>;
-
/**
* The introductory text of this Community
* Corresponds to the metadata field dc.description
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/core/shared/hal-resource.model.ts b/src/app/core/shared/hal-resource.model.ts
index 334509007b..6e4fe1f502 100644
--- a/src/app/core/shared/hal-resource.model.ts
+++ b/src/app/core/shared/hal-resource.model.ts
@@ -22,6 +22,6 @@ export class HALResource {
/**
* {@link HALLink}s to related {@link HALResource}s
*/
- [k: string]: HALLink;
+ [k: string]: HALLink | HALLink[];
};
}
diff --git a/src/app/curation-form/curation-form.component.html b/src/app/curation-form/curation-form.component.html
new file mode 100644
index 0000000000..c940494016
--- /dev/null
+++ b/src/app/curation-form/curation-form.component.html
@@ -0,0 +1,20 @@
+
diff --git a/src/app/curation-form/curation-form.component.spec.ts b/src/app/curation-form/curation-form.component.spec.ts
new file mode 100644
index 0000000000..93bad4fef2
--- /dev/null
+++ b/src/app/curation-form/curation-form.component.spec.ts
@@ -0,0 +1,165 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { CurationFormComponent } from './curation-form.component';
+import { ScriptDataService } from '../core/data/processes/script-data.service';
+import { ProcessDataService } from '../core/data/processes/process-data.service';
+import { AuthService } from '../core/auth/auth.service';
+import { of as observableOf } from 'rxjs';
+import { RequestEntry } from '../core/data/request.reducer';
+import { DSOSuccessResponse, RestResponse } from '../core/cache/response.models';
+import { Process } from '../process-page/processes/process.model';
+import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
+import { EPerson } from '../core/eperson/models/eperson.model';
+import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
+import { RouterStub } from '../shared/testing/router.stub';
+import { NotificationsService } from '../shared/notifications/notifications.service';
+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;
+ let fixture: ComponentFixture;
+
+ let scriptDataService: ScriptDataService;
+ let processDataService: ProcessDataService;
+ let configurationDataService: ConfigurationDataService;
+ let authService: AuthService;
+ let notificationsService;
+ let router;
+
+ const requestEntry = Object.assign(new RequestEntry(),
+ {response: new DSOSuccessResponse(['process-link'], 200, 'success')});
+ const failedRequestEntry = Object.assign(new RequestEntry(),
+ {response: new RestResponse(false, 400, 'Bad Request')});
+
+ const process = Object.assign(new Process(), {processId: 'process-id'});
+
+ beforeEach(async(() => {
+
+ scriptDataService = jasmine.createSpyObj('scriptDataService', {
+ invoke: observableOf(requestEntry)
+ });
+
+ processDataService = jasmine.createSpyObj('processDataService', {
+ findByHref: createSuccessfulRemoteDataObject$(process)
+ });
+
+ authService = jasmine.createSpyObj('authService', {
+ 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',
+ 'value-to-be-skipped'
+ ]
+ }))
+ });
+
+ notificationsService = new NotificationsServiceStub();
+ router = new RouterStub();
+
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot(), FormsModule, ReactiveFormsModule],
+ declarations: [CurationFormComponent],
+ providers: [
+ {provide: ScriptDataService, useValue: scriptDataService},
+ {provide: ProcessDataService, useValue: processDataService},
+ {provide: AuthService, useValue: authService},
+ {provide: NotificationsService, useValue: notificationsService},
+ {provide: Router, useValue: router},
+ {provide: ConfigurationDataService, useValue: configurationDataService},
+ ],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CurationFormComponent);
+ comp = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+ describe('init', () => {
+ it('should initialise the comp and contain the different tasks', () => {
+ expect(comp).toBeDefined();
+
+ const elements = fixture.debugElement.queryAll(By.css('option'));
+ expect(elements.length).toEqual(3);
+ expect(elements[0].nativeElement.innerHTML).toContain('curation-task.task.profileformats.label');
+ expect(elements[1].nativeElement.innerHTML).toContain('curation-task.task.requiredmetadata.label');
+ expect(elements[2].nativeElement.innerHTML).toContain('curation-task.task.checklinks.label');
+ });
+ });
+ describe('hasHandleValue', () => {
+ it('should return true when a dsoHandle value was provided', () => {
+ comp.dsoHandle = 'some-handle';
+ fixture.detectChanges();
+
+ expect(comp.hasHandleValue()).toBeTrue();
+ });
+ it('should return false when no dsoHandle value was provided', () => {
+ expect(comp.hasHandleValue()).toBeFalse();
+ });
+ });
+ describe('submit', () => {
+ it('should submit the selected process and handle to the scriptservice and navigate to the corresponding process page', () => {
+ comp.dsoHandle = 'test-handle';
+ comp.submit();
+
+ expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
+ {name: '-t', value: 'profileformats'},
+ {name: '-i', value: 'test-handle'},
+ {name: '-e', value: 'test@mail'},
+ ], []);
+ expect(notificationsService.success).toHaveBeenCalled();
+ expect(processDataService.findByHref).toHaveBeenCalledWith('process-link');
+ expect(router.navigate).toHaveBeenCalledWith(['/processes', 'process-id']);
+ });
+ it('should the selected process and handle to the scriptservice and stay on the page on error', () => {
+ (scriptDataService.invoke as jasmine.Spy).and.returnValue(observableOf(failedRequestEntry));
+
+ comp.dsoHandle = 'test-handle';
+ comp.submit();
+
+ expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
+ {name: '-t', value: 'profileformats'},
+ {name: '-i', value: 'test-handle'},
+ {name: '-e', value: 'test@mail'},
+ ], []);
+ expect(notificationsService.error).toHaveBeenCalled();
+ expect(processDataService.findByHref).not.toHaveBeenCalled();
+ expect(router.navigate).not.toHaveBeenCalled();
+ });
+ });
+ it('should use the handle provided by the form when no dsoHandle is provided', () => {
+ comp.form.get('handle').patchValue('form-handle');
+
+ comp.submit();
+
+ expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
+ {name: '-t', value: 'profileformats'},
+ {name: '-i', value: 'form-handle'},
+ {name: '-e', value: 'test@mail'},
+ ], []);
+ });
+ it('should use "all" when the handle provided by the form is empty and when no dsoHandle is provided', () => {
+
+ comp.submit();
+
+ expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
+ {name: '-t', value: 'profileformats'},
+ {name: '-i', value: 'all'},
+ {name: '-e', value: 'test@mail'},
+ ], []);
+ });
+});
diff --git a/src/app/curation-form/curation-form.component.ts b/src/app/curation-form/curation-form.component.ts
new file mode 100644
index 0000000000..95512d1129
--- /dev/null
+++ b/src/app/curation-form/curation-form.component.ts
@@ -0,0 +1,118 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { ScriptDataService } from '../core/data/processes/script-data.service';
+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, 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';
+import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
+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
+ */
+@Component({
+ selector: 'ds-curation-form',
+ templateUrl: './curation-form.component.html'
+})
+export class CurationFormComponent implements OnInit {
+
+ config: Observable>;
+ tasks: string[];
+ form: FormGroup;
+
+ @Input()
+ dsoHandle: string;
+
+ constructor(
+ private scriptDataService: ScriptDataService,
+ private configurationDataService: ConfigurationDataService,
+ private processDataService: ProcessDataService,
+ private authService: AuthService,
+ private notificationsService: NotificationsService,
+ private translateService: TranslateService,
+ private router: Router
+ ) {
+ }
+
+ ngOnInit(): void {
+ this.form = new FormGroup({
+ 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
+ .filter((value) => isNotEmpty(value) && value.includes('='))
+ .map((value) => value.split('=')[1].trim());
+ this.form.get('task').patchValue(this.tasks[0]);
+ });
+ }
+
+ /**
+ * Determines whether the inputted dsoHandle has a value
+ */
+ hasHandleValue() {
+ if (hasValue(this.dsoHandle)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Submit the selected taskName and handle to the script data service to run the corresponding curation script
+ * Navigate to the process page on success
+ */
+ submit() {
+ const taskName = this.form.get('task').value;
+ let handle;
+ if (this.hasHandleValue()) {
+ handle = this.dsoHandle;
+ } else {
+ handle = this.form.get('handle').value;
+ if (isEmpty(handle)) {
+ handle = 'all';
+ }
+ }
+ this.authService.getAuthenticatedUserFromStore().pipe(
+ take(1),
+ switchMap((eperson: EPerson) => {
+ return this.scriptDataService.invoke('curate', [
+ {name: '-t', value: taskName},
+ {name: '-i', value: handle},
+ {name: '-e', value: eperson.email},
+ ], []).pipe(getResponseFromEntry());
+ })
+ ).subscribe((response: DSOSuccessResponse) => {
+ if (response.isSuccessful) {
+ this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'),
+ this.translateService.get('curation.form.submit.success.content'));
+ this.processDataService.findByHref(response.resourceSelfLinks[0]).pipe(
+ filter((processRD: RemoteData) => hasValue(processRD) && hasValue(processRD.payload)),
+ take(1))
+ .subscribe((processRD: RemoteData) => {
+ this.router.navigate(['/processes', processRD.payload.processId]);
+ });
+ } else {
+ this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
+ this.translateService.get('curation.form.submit.error.content'));
+ }
+ });
+ }
+}
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts
index 4694c13603..6604667a98 100644
--- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts
+++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts
@@ -4,13 +4,11 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { By } from '@angular/platform-browser';
import { SharedModule } from '../../../shared.module';
import { TranslateModule } from '@ngx-translate/core';
-import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
+import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { RequestService } from '../../../../core/data/request.service';
-import { ComcolRole } from './comcol-role';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { RemoteData } from '../../../../core/data/remote-data';
import { RouterTestingModule } from '@angular/router/testing';
-import { Collection } from '../../../../core/shared/collection.model';
describe('ComcolRoleComponent', () => {
@@ -65,20 +63,10 @@ describe('ComcolRoleComponent', () => {
comp = fixture.componentInstance;
de = fixture.debugElement;
- comp.comcolRole = new ComcolRole(
- 'test role name',
- 'test role endpoint',
- );
-
- comp.dso = Object.assign(
- new Collection(), {
- _links: {
- 'test role endpoint': {
- href: 'test role link',
- }
- }
- }
- );
+ comp.comcolRole = {
+ name: 'test role name',
+ href: 'test role link',
+ };
fixture.detectChanges();
});
@@ -138,7 +126,7 @@ describe('ComcolRoleComponent', () => {
});
it('should call the groupService create method', () => {
- expect(groupService.createComcolGroup).toHaveBeenCalledWith(comp.dso, 'test role link');
+ expect(groupService.createComcolGroup).toHaveBeenCalledWith(comp.dso, 'test role name', 'test role link');
});
});
});
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts
index 41cb7e7cd2..986d616c7d 100644
--- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts
+++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts
@@ -7,9 +7,9 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Collection } from '../../../../core/shared/collection.model';
import { filter, map } from 'rxjs/operators';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
-import { ComcolRole } from './comcol-role';
import { RequestService } from '../../../../core/data/request.service';
import { RemoteData } from '../../../../core/data/remote-data';
+import { HALLink } from '../../../../core/shared/hal-link.model';
/**
* Component for managing a community or collection role.
@@ -31,7 +31,7 @@ export class ComcolRoleComponent implements OnInit {
* The role to manage
*/
@Input()
- comcolRole: ComcolRole;
+ comcolRole: HALLink;
constructor(
protected requestService: RequestService,
@@ -43,7 +43,7 @@ export class ComcolRoleComponent implements OnInit {
* The link to the related group.
*/
get groupLink(): string {
- return this.dso._links[this.comcolRole.linkName].href;
+ return this.comcolRole.href;
}
/**
@@ -106,7 +106,7 @@ export class ComcolRoleComponent implements OnInit {
* Create a group for this community or collection role.
*/
create() {
- this.groupService.createComcolGroup(this.dso, this.groupLink).subscribe();
+ this.groupService.createComcolGroup(this.dso, this.comcolRole.name, this.groupLink).subscribe();
}
/**
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts
deleted file mode 100644
index 2ac74fe67b..0000000000
--- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { Community } from '../../../../core/shared/community.model';
-import { Collection } from '../../../../core/shared/collection.model';
-
-/**
- * Class representing a community or collection role.
- */
-export class ComcolRole {
-
- /**
- * The community admin role.
- */
- public static COMMUNITY_ADMIN = new ComcolRole(
- 'community-admin',
- 'adminGroup',
- );
-
- /**
- * The collection admin role.
- */
- public static COLLECTION_ADMIN = new ComcolRole(
- 'collection-admin',
- 'adminGroup',
- );
-
- /**
- * The submitters role.
- */
- public static SUBMITTERS = new ComcolRole(
- 'submitters',
- 'submittersGroup',
- );
-
- /**
- * The default item read role.
- */
- public static ITEM_READ = new ComcolRole(
- 'item_read',
- 'itemReadGroup',
- );
-
- /**
- * The default bitstream read role.
- */
- public static BITSTREAM_READ = new ComcolRole(
- 'bitstream_read',
- 'bitstreamReadGroup',
- );
-
- /**
- * @param name The name for this community or collection role.
- * @param linkName The path linking to this community or collection role.
- */
- constructor(
- public name,
- public linkName,
- ) {
- }
-
- /**
- * Get the REST endpoint for managing this role for a given community or collection.
- * @param dso
- */
- public getEndpoint(dso: Community | Collection) {
-
- let linkPath;
- switch (dso.type + '') {
- case 'community':
- linkPath = 'communities';
- break;
- case 'collection':
- linkPath = 'collections';
- break;
- }
-
- return `${linkPath}/${dso.uuid}/${this.linkName}`;
- }
-}
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html
index aa6290ea9f..ad270aab80 100644
--- a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html
+++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html
@@ -16,7 +16,9 @@
- {{ type + '.edit.return' | translate }}
+
diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts
index dee06c29b2..9886eb28d2 100644
--- a/src/app/shared/form/form.component.ts
+++ b/src/app/shared/form/form.component.ts
@@ -318,6 +318,12 @@ export class FormComponent implements OnDestroy, OnInit {
const model = arrayContext.groups[arrayContext.groups.length - 1].group[0] as any;
if (model.type === DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN) {
model.value = Object.values(value)[0];
+ const ctrl = formArrayControl.controls[formArrayControl.length - 1];
+ const ctrlValue = ctrl.value;
+ const ctrlValueKey = Object.keys(ctrlValue)[0];
+ ctrl.setValue({
+ [ctrlValueKey]: model.value
+ });
} else if (this.formBuilderService.isQualdropGroup(model)) {
const ctrl = formArrayControl.controls[formArrayControl.length - 1];
const ctrlKey = Object.keys(ctrl.value).find((key: string) => isNotEmpty(key.match(QUALDROP_GROUP_REGEX)));
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index d1c7ed7efe..e766881f59 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -208,6 +208,7 @@ import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-
import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component';
import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component';
import { DsSelectComponent } from './ds-select/ds-select.component';
+import { CurationFormComponent } from '../curation-form/curation-form.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -472,7 +473,8 @@ const ENTRY_COMPONENTS = [
ClaimedTaskActionsReturnToPoolComponent,
ClaimedTaskActionsEditMetadataComponent,
CollectionDropdownComponent,
- FileDownloadLinkComponent
+ FileDownloadLinkComponent,
+ CurationFormComponent,
];
const SHARED_ITEM_PAGE_COMPONENTS = [
@@ -530,7 +532,8 @@ const DIRECTIVES = [
...PIPES,
...COMPONENTS,
...SHARED_ITEM_PAGE_COMPONENTS,
- ...DIRECTIVES
+ ...DIRECTIVES,
+ CurationFormComponent
],
entryComponents: [
...ENTRY_COMPONENTS
diff --git a/src/app/shared/uploader/uploader-error.model.ts b/src/app/shared/uploader/uploader-error.model.ts
new file mode 100644
index 0000000000..9238a0df36
--- /dev/null
+++ b/src/app/shared/uploader/uploader-error.model.ts
@@ -0,0 +1,9 @@
+/**
+ * An interface that represents the upload error values
+ */
+export interface UploaderError {
+ item?: any;
+ response?: any;
+ status?: any;
+ headers?: any;
+}
diff --git a/src/app/shared/uploader/uploader-options.model.ts b/src/app/shared/uploader/uploader-options.model.ts
index f195b0930e..959e5c3295 100644
--- a/src/app/shared/uploader/uploader-options.model.ts
+++ b/src/app/shared/uploader/uploader-options.model.ts
@@ -17,6 +17,11 @@ export class UploaderOptions {
*/
autoUpload = true;
+ /**
+ * Set the max number of files that can be loaded
+ */
+ maxFileNumber: number;
+
/**
* The request method to use for the file upload request
*/
diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/uploader/uploader.component.ts
index 72a38d1eb1..07f4245954 100644
--- a/src/app/shared/uploader/uploader.component.ts
+++ b/src/app/shared/uploader/uploader.component.ts
@@ -69,6 +69,11 @@ export class UploaderComponent {
*/
@Output() onUploadError: EventEmitter