Merge branch 'main' into authorities_and_controlled_vocabularies

This commit is contained in:
ddinuzzo
2020-08-14 12:53:38 +02:00
committed by GitHub
25 changed files with 382 additions and 168 deletions

View File

@@ -30,24 +30,27 @@ describe('CollectionRolesComponent', () => {
undefined, undefined,
Object.assign(new Collection(), { Object.assign(new Collection(), {
_links: { _links: {
'irrelevant': { irrelevant: {
href: 'irrelevant link', href: 'irrelevant link',
}, },
'adminGroup': { adminGroup: {
href: 'adminGroup link', href: 'adminGroup link',
}, },
'submittersGroup': { submittersGroup: {
href: 'submittersGroup link', href: 'submittersGroup link',
}, },
'itemReadGroup': { itemReadGroup: {
href: 'itemReadGroup link', href: 'itemReadGroup link',
}, },
'bitstreamReadGroup': { bitstreamReadGroup: {
href: 'bitstreamReadGroup link', href: 'bitstreamReadGroup link',
}, },
'workflowGroups/test': { workflowGroups: [
href: 'test workflow group link', {
}, name: 'test',
href: 'test workflow group link',
},
],
}, },
}), }),
), ),

View File

@@ -5,7 +5,7 @@ import { first, map } from 'rxjs/operators';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
import { ComcolRole } from '../../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role'; import { HALLink } from '../../../core/shared/hal-link.model';
/** /**
* Component for managing a collection's roles * Component for managing a collection's roles
@@ -31,19 +31,27 @@ export class CollectionRolesComponent implements OnInit {
/** /**
* The different roles for the collection, as an observable. * The different roles for the collection, as an observable.
*/ */
getComcolRoles(): Observable<ComcolRole[]> { getComcolRoles(): Observable<HALLink[]> {
return this.collection$.pipe( return this.collection$.pipe(
map((collection) => map((collection) => [
[ {
ComcolRole.COLLECTION_ADMIN, name: 'collection-admin',
ComcolRole.SUBMITTERS, href: collection._links.adminGroup.href,
ComcolRole.ITEM_READ, },
ComcolRole.BITSTREAM_READ, {
...Object.keys(collection._links) name: 'submitters',
.filter((link) => link.startsWith('workflowGroups/')) href: collection._links.submittersGroup.href,
.map((link) => new ComcolRole(link.substr('workflowGroups/'.length), link)), },
] {
), name: 'item_read',
href: collection._links.itemReadGroup.href,
},
{
name: 'bitstream_read',
href: collection._links.bitstreamReadGroup.href,
},
...collection._links.workflowGroups,
]),
); );
} }

View File

@@ -1,5 +1,5 @@
<ds-comcol-role <ds-comcol-role
*ngFor="let comcolRole of getComcolRoles()" *ngFor="let comcolRole of getComcolRoles$() | async"
[dso]="community$ | async" [dso]="community$ | async"
[comcolRole]="comcolRole" [comcolRole]="comcolRole"
> >

View File

@@ -4,8 +4,8 @@ import { Observable } from 'rxjs';
import { first, map } from 'rxjs/operators'; import { first, map } from 'rxjs/operators';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
import { ComcolRole } from '../../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { HALLink } from '../../../core/shared/hal-link.model';
/** /**
* Component for managing a community's roles * Component for managing a community's roles
@@ -31,10 +31,15 @@ export class CommunityRolesComponent implements OnInit {
/** /**
* The different roles for the community. * The different roles for the community.
*/ */
getComcolRoles(): ComcolRole[] { getComcolRoles$(): Observable<HALLink[]> {
return [ return this.community$.pipe(
ComcolRole.COMMUNITY_ADMIN, map((community) => [
]; {
name: 'community-admin',
href: community._links.adminGroup.href,
},
]),
);
} }
constructor( constructor(

View File

@@ -0,0 +1,11 @@
<div>
<div class="modal-header">{{'dso-selector.create.submission.head' | translate}}
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<ds-collection-dropdown (selectionChange)="selectObject($event.collection)">
</ds-collection-dropdown>
</div>
</div>

View File

@@ -0,0 +1,164 @@
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { CollectionSelectorComponent } from './collection-selector.component';
import { CollectionDropdownComponent } from 'src/app/shared/collection-dropdown/collection-dropdown.component';
import { Collection } from 'src/app/core/shared/collection.model';
import { of, Observable } from 'rxjs';
import { RemoteData } from 'src/app/core/data/remote-data';
import { Community } from 'src/app/core/shared/community.model';
import { FindListOptions } from 'src/app/core/data/request.models';
import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model';
import { PaginatedList } from 'src/app/core/data/paginated-list';
import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
import { PageInfo } from 'src/app/core/shared/page-info.model';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateLoaderMock } from 'src/app/shared/mocks/translate-loader.mock';
import { CollectionDataService } from 'src/app/core/data/collection-data.service';
import { ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute } from '@angular/router';
import { hot } from 'jasmine-marbles';
import { By } from '@angular/platform-browser';
describe('CollectionSelectorComponent', () => {
let component: CollectionSelectorComponent;
let fixture: ComponentFixture<CollectionSelectorComponent>;
const modal = jasmine.createSpyObj('modal', ['close', 'dismiss']);
const community: Community = Object.assign(new Community(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
name: 'Community 1'
});
const collections: Collection[] = [
Object.assign(new Collection(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
name: 'Collection 1',
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Community 1-Collection 1'
}],
parentCommunity: of(
new RemoteData(false, false, true, undefined, community, 200)
)
}),
Object.assign(new Collection(), {
id: '59ee713b-ee53-4220-8c3f-9860dc84fe33',
name: 'Collection 2',
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Community 1-Collection 2'
}],
parentCommunity: of(
new RemoteData(false, false, true, undefined, community, 200)
)
}),
Object.assign(new Collection(), {
id: 'e9dbf393-7127-415f-8919-55be34a6e9ed',
name: 'Collection 3',
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Community 1-Collection 3'
}],
parentCommunity: of(
new RemoteData(false, false, true, undefined, community, 200)
)
}),
Object.assign(new Collection(), {
id: '59da2ff0-9bf4-45bf-88be-e35abd33f304',
name: 'Collection 4',
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Community 1-Collection 4'
}],
parentCommunity: of(
new RemoteData(false, false, true, undefined, community, 200)
)
}),
Object.assign(new Collection(), {
id: 'a5159760-f362-4659-9e81-e3253ad91ede',
name: 'Collection 5',
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Community 1-Collection 5'
}],
parentCommunity: of(
new RemoteData(false, false, true, undefined, community, 200)
)
})
];
// tslint:disable-next-line: max-classes-per-file
const collectionDataServiceMock = {
getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Collection>>): Observable<RemoteData<PaginatedList<Collection>>> {
return hot( 'a|', {
a: createSuccessfulRemoteDataObject(
new PaginatedList(new PageInfo(), collections)
)
});
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
})
],
declarations: [ CollectionSelectorComponent, CollectionDropdownComponent ],
providers: [
{provide: CollectionDataService, useValue: collectionDataServiceMock},
{provide: ChangeDetectorRef, useValue: {}},
{provide: ElementRef, userValue: {}},
{provide: NgbActiveModal, useValue: modal},
{provide: ActivatedRoute, useValue: {}}
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CollectionSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call selectObject', fakeAsync(() => {
spyOn(component, 'selectObject');
fixture.detectChanges();
tick();
fixture.whenStable().then(() => {
const collectionItem = fixture.debugElement.query(By.css('.collection-item:nth-child(2)'));
collectionItem.triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
expect(component.selectObject).toHaveBeenCalled();
});
}));
it('should close the dialog', () => {
component.close();
expect((component as any).activeModal.close).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,33 @@
import { Component } from '@angular/core';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
/**
* This component displays the dialog that shows the list of selectable collections
* on the MyDSpace page
*/
@Component({
selector: 'ds-collection-selector',
templateUrl: './collection-selector.component.html',
styleUrls: ['./collection-selector.component.scss']
})
export class CollectionSelectorComponent {
constructor(protected activeModal: NgbActiveModal) {}
/**
* Method called when an element has been selected from collection list.
* Its close the active modal and send selected value to the component container
* @param dso The selected DSpaceObject
*/
selectObject(dso: DSpaceObject) {
this.activeModal.close(dso);
}
/**
* Close the modal
*/
close() {
this.activeModal.close();
}
}

View File

@@ -3,7 +3,8 @@
<ds-uploader *ngIf="uploadFilesOptions.url !== ''" <ds-uploader *ngIf="uploadFilesOptions.url !== ''"
[uploadFilesOptions]="uploadFilesOptions" [uploadFilesOptions]="uploadFilesOptions"
(onCompleteItem)="onCompleteItem($event)" (onCompleteItem)="onCompleteItem($event)"
(onUploadError)="onUploadError()"></ds-uploader> (onUploadError)="onUploadError($event)"
(onFileSelected)="afterFileLoaded($event)"></ds-uploader>
</div> </div>
<div class="add"> <div class="add">

View File

@@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; 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 { RouterTestingModule } from '@angular/router/testing';
import { Store } from '@ngrx/store'; 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 { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission.component';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
@@ -23,10 +22,25 @@ import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.moc
import { UploaderService } from '../../shared/uploader/uploader.service'; import { UploaderService } from '../../shared/uploader/uploader.service';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { UploaderComponent } from 'src/app/shared/uploader/uploader.component';
describe('MyDSpaceNewSubmissionComponent test', () => { describe('MyDSpaceNewSubmissionComponent test', () => {
const translateService: any = getMockTranslateService(); 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 store: Store<AppState> = jasmine.createSpyObj('store', { const store: Store<AppState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
dispatch: {}, dispatch: {},
@@ -56,11 +70,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
{ provide: ScrollToService, useValue: getMockScrollToService() }, { provide: ScrollToService, useValue: getMockScrollToService() },
{ provide: Store, useValue: store }, { provide: Store, useValue: store },
{ provide: TranslateService, useValue: translateService }, { provide: TranslateService, useValue: translateService },
{ { provide: NgbModal, useValue: modalService },
provide: NgbModal, useValue: {
open: () => {/*comment*/}
}
},
ChangeDetectorRef, ChangeDetectorRef,
MyDSpaceNewSubmissionComponent, MyDSpaceNewSubmissionComponent,
UploaderService UploaderService
@@ -100,6 +110,10 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(MyDSpaceNewSubmissionComponent); fixture = TestBed.createComponent(MyDSpaceNewSubmissionComponent);
comp = fixture.componentInstance; 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', () => { it('should call app.openDialog', () => {
@@ -111,6 +125,12 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
}); });
expect(comp.openDialog).toHaveBeenCalled(); 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();
});
}); });
}); });

View File

@@ -1,11 +1,8 @@
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { SubmissionState } from '../../submission/submission.reducers';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -15,9 +12,11 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { NotificationType } from '../../shared/notifications/models/notification-type'; import { NotificationType } from '../../shared/notifications/models/notification-type';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { SearchResult } from '../../shared/search/search-result.model'; import { SearchResult } from '../../shared/search/search-result.model';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { CreateItemParentSelectorComponent } from 'src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; import { CreateItemParentSelectorComponent } from 'src/app/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 'src/app/shared/uploader/uploader.component';
import { UploaderError } from 'src/app/shared/uploader/uploader-error.model';
/** /**
* This component represents the whole mydspace page header * This component represents the whole mydspace page header
@@ -43,6 +42,11 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
*/ */
private sub: Subscription; private sub: Subscription;
/**
* Reference to uploaderComponent
*/
@ViewChild(UploaderComponent, { static: false }) uploaderComponent: UploaderComponent;
/** /**
* Initialize instance variables * Initialize instance variables
* *
@@ -57,9 +61,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private halService: HALEndpointService, private halService: HALEndpointService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private store: Store<SubmissionState>,
private translate: TranslateService, private translate: TranslateService,
private router: Router,
private modalService: NgbModal) { private modalService: NgbModal) {
} }
@@ -67,6 +69,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
* Initialize url and Bearer token * Initialize url and Bearer token
*/ */
ngOnInit() { ngOnInit() {
this.uploadFilesOptions.autoUpload = false;
this.sub = this.halService.getEndpoint('workspaceitems').pipe(first()).subscribe((url) => { this.sub = this.halService.getEndpoint('workspaceitems').pipe(first()).subscribe((url) => {
this.uploadFilesOptions.url = url; this.uploadFilesOptions.url = url;
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
@@ -106,8 +109,12 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
/** /**
* Method called on file upload error * Method called on file upload error
*/ */
public onUploadError() { public onUploadError(error: UploaderError) {
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed')); 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));
} }
/** /**
@@ -118,6 +125,28 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
this.modalService.open(CreateItemParentSelectorComponent); 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 * Unsubscribe from the subscription
*/ */

View File

@@ -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 { 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 { 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 { 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({ @NgModule({
imports: [ imports: [
@@ -40,7 +41,8 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/
ClaimedTaskSearchResultDetailElementComponent, ClaimedTaskSearchResultDetailElementComponent,
PoolSearchResultDetailElementComponent, PoolSearchResultDetailElementComponent,
MyDSpaceNewSubmissionComponent, MyDSpaceNewSubmissionComponent,
ItemSearchResultListElementSubmissionComponent ItemSearchResultListElementSubmissionComponent,
CollectionSelectorComponent
], ],
providers: [ providers: [
MyDSpaceGuard, MyDSpaceGuard,
@@ -57,7 +59,8 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/
WorkflowItemSearchResultDetailElementComponent, WorkflowItemSearchResultDetailElementComponent,
ClaimedTaskSearchResultDetailElementComponent, ClaimedTaskSearchResultDetailElementComponent,
PoolSearchResultDetailElementComponent, PoolSearchResultDetailElementComponent,
ItemSearchResultListElementSubmissionComponent ItemSearchResultListElementSubmissionComponent,
CollectionSelectorComponent
] ]
}) })

View File

@@ -317,16 +317,17 @@ export class GroupDataService extends DataService<Group> {
* Create a group for a given role for a given community or collection. * 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 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 * @param link The REST endpoint to create the group
*/ */
createComcolGroup(dso: Community|Collection, link: string): Observable<RestResponse> { createComcolGroup(dso: Community|Collection, role: string, link: string): Observable<RestResponse> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const group = Object.assign(new Group(), { const group = Object.assign(new Group(), {
metadata: { metadata: {
'dc.description': [ 'dc.description': [
{ {
value: `${this.nameService.getName(dso)} admin group`, value: `${this.nameService.getName(dso)} ${role} group`,
} }
], ],
}, },

View File

@@ -15,8 +15,6 @@ import { RESOURCE_POLICY } from '../resource-policy/models/resource-policy.resou
import { COMMUNITY } from './community.resource-type'; import { COMMUNITY } from './community.resource-type';
import { Community } from './community.model'; import { Community } from './community.model';
import { ChildHALResource } from './child-hal-resource.model'; import { ChildHALResource } from './child-hal-resource.model';
import { GROUP } from '../eperson/models/group.resource-type';
import { Group } from '../eperson/models/group.model';
@typedObject @typedObject
@inheritSerialization(DSpaceObject) @inheritSerialization(DSpaceObject)
@@ -41,6 +39,11 @@ export class Collection extends DSpaceObject implements ChildHALResource {
defaultAccessConditions: HALLink; defaultAccessConditions: HALLink;
logo: HALLink; logo: HALLink;
parentCommunity: HALLink; parentCommunity: HALLink;
workflowGroups: HALLink[];
adminGroup: HALLink;
submittersGroup: HALLink;
itemReadGroup: HALLink;
bitstreamReadGroup: HALLink;
self: HALLink; self: HALLink;
}; };
@@ -72,12 +75,6 @@ export class Collection extends DSpaceObject implements ChildHALResource {
@link(COMMUNITY, false) @link(COMMUNITY, false)
parentCommunity?: Observable<RemoteData<Community>>; parentCommunity?: Observable<RemoteData<Community>>;
/**
* The administrators group of this community.
*/
@link(GROUP)
adminGroup?: Observable<RemoteData<Group>>;
/** /**
* The introductory text of this Collection * The introductory text of this Collection
* Corresponds to the metadata field dc.description * Corresponds to the metadata field dc.description

View File

@@ -3,8 +3,6 @@ import { Observable } from 'rxjs';
import { link, typedObject } from '../cache/builders/build-decorators'; import { link, typedObject } from '../cache/builders/build-decorators';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data'; 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.model';
import { BITSTREAM } from './bitstream.resource-type'; import { BITSTREAM } from './bitstream.resource-type';
import { Collection } from './collection.model'; import { Collection } from './collection.model';
@@ -66,12 +64,6 @@ export class Community extends DSpaceObject implements ChildHALResource {
@link(COMMUNITY, false) @link(COMMUNITY, false)
parentCommunity?: Observable<RemoteData<Community>>; parentCommunity?: Observable<RemoteData<Community>>;
/**
* The administrators group of this community.
*/
@link(GROUP)
adminGroup?: Observable<RemoteData<Group>>;
/** /**
* The introductory text of this Community * The introductory text of this Community
* Corresponds to the metadata field dc.description * Corresponds to the metadata field dc.description

View File

@@ -22,6 +22,6 @@ export class HALResource {
/** /**
* {@link HALLink}s to related {@link HALResource}s * {@link HALLink}s to related {@link HALResource}s
*/ */
[k: string]: HALLink; [k: string]: HALLink | HALLink[];
}; };
} }

View File

@@ -4,13 +4,11 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { SharedModule } from '../../../shared.module'; import { SharedModule } from '../../../shared.module';
import { TranslateModule } from '@ngx-translate/core'; 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 { RequestService } from '../../../../core/data/request.service';
import { ComcolRole } from './comcol-role';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs/internal/observable/of';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { Collection } from '../../../../core/shared/collection.model';
describe('ComcolRoleComponent', () => { describe('ComcolRoleComponent', () => {
@@ -65,20 +63,10 @@ describe('ComcolRoleComponent', () => {
comp = fixture.componentInstance; comp = fixture.componentInstance;
de = fixture.debugElement; de = fixture.debugElement;
comp.comcolRole = new ComcolRole( comp.comcolRole = {
'test role name', name: 'test role name',
'test role endpoint', href: 'test role link',
); };
comp.dso = Object.assign(
new Collection(), {
_links: {
'test role endpoint': {
href: 'test role link',
}
}
}
);
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -138,7 +126,7 @@ describe('ComcolRoleComponent', () => {
}); });
it('should call the groupService create method', () => { 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');
}); });
}); });
}); });

View File

@@ -7,9 +7,9 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
import { filter, map } from 'rxjs/operators'; import { filter, map } from 'rxjs/operators';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { ComcolRole } from './comcol-role';
import { RequestService } from '../../../../core/data/request.service'; import { RequestService } from '../../../../core/data/request.service';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { HALLink } from '../../../../core/shared/hal-link.model';
/** /**
* Component for managing a community or collection role. * Component for managing a community or collection role.
@@ -31,7 +31,7 @@ export class ComcolRoleComponent implements OnInit {
* The role to manage * The role to manage
*/ */
@Input() @Input()
comcolRole: ComcolRole; comcolRole: HALLink;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
@@ -43,7 +43,7 @@ export class ComcolRoleComponent implements OnInit {
* The link to the related group. * The link to the related group.
*/ */
get groupLink(): string { 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 a group for this community or collection role.
*/ */
create() { create() {
this.groupService.createComcolGroup(this.dso, this.groupLink).subscribe(); this.groupService.createComcolGroup(this.dso, this.comcolRole.name, this.groupLink).subscribe();
} }
/** /**

View File

@@ -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}`;
}
}

View File

@@ -319,6 +319,12 @@ export class FormComponent implements OnDestroy, OnInit {
const model = arrayContext.groups[arrayContext.groups.length - 1].group[0] as any; const model = arrayContext.groups[arrayContext.groups.length - 1].group[0] as any;
if (model.type === DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN || model.type === DYNAMIC_FORM_CONTROL_TYPE_ONEBOX) { if (model.type === DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN || model.type === DYNAMIC_FORM_CONTROL_TYPE_ONEBOX) {
model.value = Object.values(value)[0]; 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)) { } else if (this.formBuilderService.isQualdropGroup(model)) {
const ctrl = formArrayControl.controls[formArrayControl.length - 1]; const ctrl = formArrayControl.controls[formArrayControl.length - 1];
const ctrlKey = Object.keys(ctrl.value).find((key: string) => isNotEmpty(key.match(QUALDROP_GROUP_REGEX))); const ctrlKey = Object.keys(ctrl.value).find((key: string) => isNotEmpty(key.match(QUALDROP_GROUP_REGEX)));

View File

@@ -0,0 +1,9 @@
/**
* An interface that represents the upload error values
*/
export interface UploaderError {
item?: any;
response?: any;
status?: any;
headers?: any;
}

View File

@@ -17,6 +17,11 @@ export class UploaderOptions {
*/ */
autoUpload = true; autoUpload = true;
/**
* Set the max number of files that can be loaded
*/
maxFileNumber: number;
/** /**
* The request method to use for the file upload request * The request method to use for the file upload request
*/ */

View File

@@ -69,6 +69,11 @@ export class UploaderComponent {
*/ */
@Output() onUploadError: EventEmitter<any> = new EventEmitter<any>(); @Output() onUploadError: EventEmitter<any> = new EventEmitter<any>();
/**
* The function to call when a file is selected
*/
@Output() onFileSelected: EventEmitter<any> = new EventEmitter<any>();
public uploader: FileUploader; public uploader: FileUploader;
public uploaderId: string; public uploaderId: string;
public isOverBaseDropZone = observableOf(false); public isOverBaseDropZone = observableOf(false);
@@ -102,7 +107,8 @@ export class UploaderComponent {
itemAlias: this.uploadFilesOptions.itemAlias, itemAlias: this.uploadFilesOptions.itemAlias,
removeAfterUpload: true, removeAfterUpload: true,
autoUpload: this.uploadFilesOptions.autoUpload, autoUpload: this.uploadFilesOptions.autoUpload,
method: this.uploadFilesOptions.method method: this.uploadFilesOptions.method,
queueLimit: this.uploadFilesOptions.maxFileNumber
}); });
if (isUndefined(this.enableDragOverDocument)) { if (isUndefined(this.enableDragOverDocument)) {
@@ -121,6 +127,9 @@ export class UploaderComponent {
this.uploader.onAfterAddingFile = ((item) => { this.uploader.onAfterAddingFile = ((item) => {
item.withCredentials = false; item.withCredentials = false;
}); });
this.uploader.onAfterAddingAll = ((items) => {
this.onFileSelected.emit(items);
});
if (isUndefined(this.onBeforeUpload)) { if (isUndefined(this.onBeforeUpload)) {
this.onBeforeUpload = () => {return}; this.onBeforeUpload = () => {return};
} }
@@ -149,7 +158,7 @@ export class UploaderComponent {
} }
}; };
this.uploader.onErrorItem = (item: any, response: any, status: any, headers: any) => { this.uploader.onErrorItem = (item: any, response: any, status: any, headers: any) => {
this.onUploadError.emit(null); this.onUploadError.emit({ item: item, response: response, status: status, headers: headers });
this.uploader.cancelAll(); this.uploader.cancelAll();
}; };
this.uploader.onProgressAll = () => this.onProgress(); this.uploader.onProgressAll = () => this.onProgress();

View File

@@ -29,6 +29,7 @@ import { FormFieldMetadataValueObject } from '../../../shared/form/builder/model
import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { deepClone } from 'fast-json-patch';
/** /**
* The service handling all form section operations * The service handling all form section operations
@@ -312,7 +313,7 @@ export class SectionFormOperationsService {
event: DynamicFormControlEvent event: DynamicFormControlEvent
): void { ): void {
const path = this.getFieldPathSegmentedFromChangeEvent(event); const path = this.getFieldPathSegmentedFromChangeEvent(event);
const value = this.getFieldValueFromChangeEvent(event); const value = deepClone(this.getFieldValueFromChangeEvent(event));
if (isNotEmpty(value)) { if (isNotEmpty(value)) {
value.place = this.getArrayIndexFromEvent(event); value.place = this.getArrayIndexFromEvent(event);
if (hasValue(event.group) && hasValue(event.group.value)) { if (hasValue(event.group) && hasValue(event.group.value)) {

View File

@@ -959,6 +959,8 @@
"dso-selector.create.item.head": "New item", "dso-selector.create.item.head": "New item",
"dso-selector.create.submission.head": "New submission",
"dso-selector.edit.collection.head": "Edit collection", "dso-selector.edit.collection.head": "Edit collection",
"dso-selector.edit.community.head": "Edit community", "dso-selector.edit.community.head": "Edit community",
@@ -1955,6 +1957,10 @@
"mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.",
"mydspace.upload.upload-failed-manyentries": "Unprocessable file. Detected too many entries but allowed only one for file.",
"mydspace.upload.upload-failed-moreonefile": "Unprocessable request. Only one file is allowed.",
"mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.",
"mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.",