Merge branch 'master' into w2p-64503_Edit-collection-Content-Source-2

Conflicts:
	resources/i18n/en.json5
	src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts
	src/app/+community-page/edit-community-page/edit-community-page.component.ts
	src/app/shared/shared.module.ts
This commit is contained in:
Kristof De Langhe
2020-01-09 10:34:08 +01:00
41 changed files with 993 additions and 188 deletions

View File

@@ -244,6 +244,8 @@
"collection.create.head": "Create a Collection",
"collection.create.notifications.success": "Successfully created the Collection",
"collection.create.sub-head": "Create a Collection for Community {{ parent }}",
"collection.delete.cancel": "Cancel",
@@ -302,6 +304,24 @@
"collection.edit.logo.label": "Collection logo",
"collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.",
"collection.edit.logo.notifications.add.success": "Upload Collection logo successful.",
"collection.edit.logo.notifications.delete.success.title": "Logo deleted",
"collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo",
"collection.edit.logo.notifications.delete.error.title": "Error deleting logo",
"collection.edit.logo.upload": "Drop a Collection Logo to upload",
"collection.edit.notifications.success": "Successfully edited the Collection",
"collection.edit.return": "Return",
@@ -410,6 +430,8 @@
"community.create.head": "Create a Community",
"community.create.notifications.success": "Successfully created the Community",
"community.create.sub-head": "Create a Sub-Community for Community {{ parent }}",
"community.delete.cancel": "Cancel",
@@ -428,8 +450,30 @@
"community.edit.head": "Edit Community",
"community.edit.logo.label": "Community logo",
"community.edit.logo.notifications.add.error": "Uploading Community logo failed. Please verify the content before retrying.",
"community.edit.logo.notifications.add.success": "Upload Community logo successful.",
"community.edit.logo.notifications.delete.success.title": "Logo deleted",
"community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo",
"community.edit.logo.notifications.delete.error.title": "Error deleting logo",
"community.edit.logo.upload": "Drop a Community Logo to upload",
"community.edit.notifications.success": "Successfully edited the Community",
"community.edit.return": "Return",
"community.edit.tabs.curate.head": "Curate",
"community.edit.tabs.curate.title": "Community Edit - Curate",
@@ -442,6 +486,8 @@
"community.edit.tabs.roles.title": "Community Edit - Roles",
"community.form.abstract": "Short Description",
"community.form.description": "Introductory text (HTML)",
@@ -1847,7 +1893,7 @@
"uploader.drag-message": "Drag & Drop your files here",
"uploader.or": ", or",
"uploader.or": ", or ",
"uploader.processing": "Processing",

View File

@@ -1,9 +1,15 @@
import { Component, Input } from '@angular/core';
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicFormService, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
import { Collection } from '../../core/shared/collection.model';
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CommunityDataService } from '../../core/data/community-data.service';
import { AuthService } from '../../core/auth/auth.service';
import { RequestService } from '../../core/data/request.service';
import { ObjectCacheService } from '../../core/cache/object-cache.service';
/**
* Form used for creating and editing collections
@@ -22,7 +28,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
/**
* @type {Collection.type} This is a collection-type form
*/
protected type = Collection.type;
type = Collection.type;
/**
* The dynamic form fields used for creating/editing a collection
@@ -65,4 +71,15 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
name: 'dc.description.provenance',
}),
];
public constructor(protected location: Location,
protected formService: DynamicFormService,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected dsoService: CommunityDataService,
protected requestService: RequestService,
protected objectCache: ObjectCacheService) {
super(location, formService, translate, notificationsService, authService, requestService, objectCache);
}
}

View File

@@ -5,15 +5,17 @@
<div *ngIf="collectionRD?.payload as collection">
<ds-view-tracker [object]="collection"></ds-view-tracker>
<header class="comcol-header border-bottom mb-4 pb-4">
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload" [alternateText]="'Collection Logo'">
[alternateText]="'Collection Logo'">
</ds-comcol-page-logo>
<!-- Collection Name -->
<!-- Collection Name -->
<ds-comcol-page-header
[name]="collection.name">
</ds-comcol-page-header>
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload"
[alternateText]="'Collection Logo'"
[alternateText]="'Collection Logo'">
</ds-comcol-page-logo>
<!-- Handle -->
<ds-comcol-page-handle
[content]="collection.handle"

View File

@@ -4,5 +4,5 @@
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2>
</div>
</div>
<ds-collection-form (submitForm)="onSubmit($event)"></ds-collection-form>
<ds-collection-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-collection-form>
</div>

View File

@@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
import { of as observableOf } from 'rxjs';
import { CommunityDataService } from '../../core/data/community-data.service';
import { CreateCollectionPageComponent } from './create-collection-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
describe('CreateCollectionPageComponent', () => {
let comp: CreateCollectionPageComponent;
@@ -27,6 +29,7 @@ describe('CreateCollectionPageComponent', () => {
},
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
{ provide: Router, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -5,6 +5,8 @@ import { Router } from '@angular/router';
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
import { Collection } from '../../core/shared/collection.model';
import { CollectionDataService } from '../../core/data/collection-data.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
/**
* Component that represents the page where a user can create a new Collection
@@ -16,13 +18,16 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
})
export class CreateCollectionPageComponent extends CreateComColPageComponent<Collection> {
protected frontendURL = '/collections/';
protected type = Collection.type;
public constructor(
protected communityDataService: CommunityDataService,
protected collectionDataService: CollectionDataService,
protected routeService: RouteService,
protected router: Router
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
super(collectionDataService, communityDataService, routeService, router);
super(collectionDataService, communityDataService, routeService, router, notificationsService, translate);
}
}

View File

@@ -1,4 +1,6 @@
<ds-collection-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-collection-form>
<ds-collection-form (submitForm)="onSubmit($event)"
[dso]="(dsoRD$ | async)?.payload"
(finish)="navigateToHomePage()"></ds-collection-form>
<a class="btn btn-danger"
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete'
| translate}}</a>

View File

@@ -8,6 +8,8 @@ import { ActivatedRoute } from '@angular/router';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CollectionMetadataComponent } from './collection-metadata.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
describe('CollectionMetadataComponent', () => {
let comp: CollectionMetadataComponent;
@@ -20,6 +22,7 @@ describe('CollectionMetadataComponent', () => {
providers: [
{ provide: CollectionDataService, useValue: {} },
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -3,6 +3,8 @@ import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comco
import { Collection } from '../../../core/shared/collection.model';
import { CollectionDataService } from '../../../core/data/collection-data.service';
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
/**
* Component for editing a collection's metadata
@@ -13,12 +15,15 @@ import { ActivatedRoute, Router } from '@angular/router';
})
export class CollectionMetadataComponent extends ComcolMetadataComponent<Collection> {
protected frontendURL = '/collections/';
protected type = Collection.type;
public constructor(
protected collectionDataService: CollectionDataService,
protected router: Router,
protected route: ActivatedRoute
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
super(collectionDataService, router, route);
super(collectionDataService, router, route, notificationsService, translate);
}
}

View File

@@ -12,7 +12,7 @@ import { getCollectionPageRoute } from '../collection-page-routing.module';
templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
})
export class EditCollectionPageComponent extends EditComColPageComponent<Collection> {
public type = 'collection';
type = 'collection';
public constructor(
protected router: Router,

View File

@@ -28,7 +28,10 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
{
path: 'metadata',
component: CollectionMetadataComponent,
data: { title: 'collection.edit.tabs.metadata.title' }
data: {
title: 'collection.edit.tabs.metadata.title',
hideReturnButton: true
}
},
{
path: 'roles',

View File

@@ -1,9 +1,16 @@
import { Component, Input } from '@angular/core';
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicFormService, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
import { Community } from '../../core/shared/community.model';
import { ResourceType } from '../../core/shared/resource-type';
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CommunityDataService } from '../../core/data/community-data.service';
import { AuthService } from '../../core/auth/auth.service';
import { RequestService } from '../../core/data/request.service';
import { ObjectCacheService } from '../../core/cache/object-cache.service';
/**
* Form used for creating and editing communities
@@ -22,7 +29,7 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
/**
* @type {Community.type} This is a community-type form
*/
protected type = Community.type;
type = Community.type;
/**
* The dynamic form fields used for creating/editing a community
@@ -57,4 +64,15 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
name: 'dc.description.tableofcontents',
}),
];
public constructor(protected location: Location,
protected formService: DynamicFormService,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected dsoService: CommunityDataService,
protected requestService: RequestService,
protected objectCache: ObjectCacheService) {
super(location, formService, translate, notificationsService, authService, requestService, objectCache);
}
}

View File

@@ -3,12 +3,11 @@
<div *ngIf="communityRD?.payload; let communityPayload">
<ds-view-tracker [object]="communityPayload"></ds-view-tracker>
<header class="comcol-header border-bottom mb-4 pb-4">
<!-- Community name -->
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
<!-- Community logo -->
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
</ds-comcol-page-logo>
<!-- Community name -->
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
<!-- Handle -->
<ds-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'">
</ds-comcol-page-handle>

View File

@@ -7,5 +7,5 @@
</ng-container>
</div>
</div>
<ds-community-form (submitForm)="onSubmit($event)"></ds-community-form>
<ds-community-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-community-form>
</div>

View File

@@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
import { of as observableOf } from 'rxjs';
import { CommunityDataService } from '../../core/data/community-data.service';
import { CreateCommunityPageComponent } from './create-community-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
describe('CreateCommunityPageComponent', () => {
let comp: CreateCommunityPageComponent;
@@ -23,6 +25,7 @@ describe('CreateCommunityPageComponent', () => {
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
{ provide: Router, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -4,6 +4,8 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { RouteService } from '../../core/services/route.service';
import { Router } from '@angular/router';
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
/**
* Component that represents the page where a user can create a new Community
@@ -15,12 +17,15 @@ import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comc
})
export class CreateCommunityPageComponent extends CreateComColPageComponent<Community> {
protected frontendURL = '/communities/';
protected type = Community.type;
public constructor(
protected communityDataService: CommunityDataService,
protected routeService: RouteService,
protected router: Router
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
super(communityDataService, communityDataService, routeService, router);
super(communityDataService, communityDataService, routeService, router, notificationsService, translate);
}
}

View File

@@ -1,4 +1,6 @@
<ds-community-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-community-form>
<ds-community-form (submitForm)="onSubmit($event)"
[dso]="(dsoRD$ | async)?.payload"
(finish)="navigateToHomePage()"></ds-community-form>
<a class="btn btn-danger"
[routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete'
| translate}}</a>

View File

@@ -8,6 +8,8 @@ import { of as observableOf } from 'rxjs/internal/observable/of';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CommunityMetadataComponent } from './community-metadata.component';
import { CommunityDataService } from '../../../core/data/community-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
describe('CommunityMetadataComponent', () => {
let comp: CommunityMetadataComponent;
@@ -20,6 +22,7 @@ describe('CommunityMetadataComponent', () => {
providers: [
{ provide: CommunityDataService, useValue: {} },
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -3,6 +3,8 @@ import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comco
import { ActivatedRoute, Router } from '@angular/router';
import { Community } from '../../../core/shared/community.model';
import { CommunityDataService } from '../../../core/data/community-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
/**
* Component for editing a community's metadata
@@ -13,12 +15,15 @@ import { CommunityDataService } from '../../../core/data/community-data.service'
})
export class CommunityMetadataComponent extends ComcolMetadataComponent<Community> {
protected frontendURL = '/communities/';
protected type = Community.type;
public constructor(
protected communityDataService: CommunityDataService,
protected router: Router,
protected route: ActivatedRoute
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
super(communityDataService, router, route);
super(communityDataService, router, route, notificationsService, translate);
}
}

View File

@@ -12,7 +12,7 @@ import { getCommunityPageRoute } from '../community-page-routing.module';
templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
})
export class EditCommunityPageComponent extends EditComColPageComponent<Community> {
public type = 'community';
type = 'community';
public constructor(
protected router: Router,

View File

@@ -27,7 +27,10 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
{
path: 'metadata',
component: CommunityMetadataComponent,
data: { title: 'community.edit.tabs.metadata.title' }
data: {
title: 'community.edit.tabs.metadata.title',
hideReturnButton: true
}
},
{
path: 'roles',

View File

@@ -33,12 +33,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
/**
* The UploaderOptions object
*/
public uploadFilesOptions: UploaderOptions = {
url: '',
authToken: null,
disableMultipart: false,
itemAlias: null
};
public uploadFilesOptions: UploaderOptions = new UploaderOptions();
/**
* Subscription to unsubscribe from

View File

@@ -1,32 +1,41 @@
import {
distinctUntilChanged,
filter, first,
map,
mergeMap,
share,
switchMap,
filter, first,map, mergeMap, share, switchMap,
take,
tap
} from 'rxjs/operators';
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
import { merge as observableMerge, Observable, throwError as observableThrowError, combineLatest as observableCombineLatest } from 'rxjs';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { CommunityDataService } from './community-data.service';
import { DataService } from './data.service';
import { DeleteRequest, FindListOptions, FindByIDRequest, RestRequest } from './request.models';
import { PaginatedList } from './paginated-list';
import { RemoteData } from './remote-data';
import { FindListOptions, FindByIDRequest } from './request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { getResponseFromEntry } from '../shared/operators';
import {
configureRequest,
getRemoteDataPayload,
getResponseFromEntry,
getSucceededRemoteData
} from '../shared/operators';
import { CacheableObject } from '../cache/object-cache.reducer';
import { RestResponse } from '../cache/response.models';
import { Bitstream } from '../shared/bitstream.model';
import { DSpaceObject } from '../shared/dspace-object.model';
export abstract class ComColDataService<T extends CacheableObject> extends DataService<T> {
protected abstract cds: CommunityDataService;
protected abstract objectCache: ObjectCacheService;
protected abstract halService: HALEndpointService;
/**
* Linkpath of endpoint to delete the logo
*/
protected logoDeleteLinkpath = 'bitstreams';
/**
* Get the scoped endpoint URL by fetching the object with
* the given scopeID and returning its HAL link with this
@@ -76,4 +85,33 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS
return this.findList(href$, options);
}
/**
* Get the endpoint for the community or collection's logo
* @param id The community or collection's ID
*/
public getLogoEndpoint(id: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
switchMap((href: string) => this.halService.getEndpoint('logo', `${href}/${id}`))
)
}
/**
* Delete the logo from the community or collection
* @param dso The object to delete the logo from
*/
public deleteLogo(dso: DSpaceObject): Observable<RestResponse> {
const logo$ = (dso as any).logo;
if (hasValue(logo$)) {
return observableCombineLatest(
logo$.pipe(getSucceededRemoteData(), getRemoteDataPayload(), take(1)),
this.halService.getEndpoint(this.logoDeleteLinkpath)
).pipe(
map(([logo, href]: [Bitstream, string]) => `${href}/${logo.id}`),
map((href: string) => new DeleteRequest(this.requestService.generateRequestId(), href)),
configureRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
getResponseFromEntry()
);
}
}
}

View File

@@ -1,3 +1,38 @@
<div class="container-fluid">
<div class="row">
<div class="col-12 d-inline-block">
<label>{{type.value + '.edit.logo.label' | translate}}</label>
</div>
<ng-container *ngVar="(dso?.logo | async)?.payload as logo">
<div class="col-12 d-inline-block alert" [ngClass]="{'alert-danger': markLogoForDeletion}" id="logo-section">
<div class="row">
<div class="col-8 d-inline-block">
<ds-comcol-page-logo [logo]="logo"></ds-comcol-page-logo>
</div>
<div class="col-4 d-inline-block">
<div *ngIf="logo" class="btn-group btn-group-sm float-right" role="group">
<button *ngIf="!markLogoForDeletion" type="button" class="btn btn-danger" (click)="deleteLogo()">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
<button *ngIf="markLogoForDeletion" type="button" class="btn btn-warning" (click)="undoDeleteLogo()">
<i class="fas fa-undo" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<div *ngIf="!logo" class="col-12 d-inline-block">
<ds-uploader *ngIf="initializedUploaderOptions | async"
[dropMsg]="type.value + '.edit.logo.upload'"
[dropOverDocumentMsg]="type.value + '.edit.logo.upload'"
[enableDragOverDocument]="true"
[uploadFilesOptions]="uploadFilesOptions"
(onCompleteItem)="onCompleteItem()"
(onUploadError)="onUploadError()"></ds-uploader>
</div>
</ng-container>
</div>
</div>
<ds-form *ngIf="formModel"
[formId]="'comcol-form-id'"
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>

View File

@@ -7,10 +7,22 @@ import { DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core';
import { FormControl, FormGroup } from '@angular/forms';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
import { Community } from '../../../core/shared/community.model';
import { ResourceType } from '../../../core/shared/resource-type';
import { ComColFormComponent } from './comcol-form.component';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { hasValue } from '../../empty.util';
import { VarDirective } from '../../utils/var.directive';
import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../testing/notifications-service-stub';
import { AuthService } from '../../../core/auth/auth.service';
import { AuthServiceMock } from '../../mocks/mock-auth.service';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { RestRequestMethod } from '../../../core/data/rest-request-method';
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
import { RequestError } from '../../../core/data/request.models';
import { RequestService } from '../../../core/data/request.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { By } from '@angular/platform-browser';
describe('ComColFormComponent', () => {
let comp: ComColFormComponent<DSpaceObject>;
@@ -49,71 +61,264 @@ describe('ComColFormComponent', () => {
})
];
const logoEndpoint = 'rest/api/logo/endpoint';
const dsoService = Object.assign({
getLogoEndpoint: () => observableOf(logoEndpoint),
deleteLogo: () => observableOf({})
});
const notificationsService = new NotificationsServiceStub();
/* tslint:disable:no-empty */
const locationStub = jasmine.createSpyObj('location', ['back']);
/* tslint:enable:no-empty */
const requestServiceStub = jasmine.createSpyObj({
removeByHrefSubstring: {}
});
const objectCacheStub = jasmine.createSpyObj({
remove: {}
});
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule],
declarations: [ComColFormComponent],
declarations: [ComColFormComponent, VarDirective],
providers: [
{ provide: Location, useValue: locationStub },
{ provide: DynamicFormService, useValue: formServiceStub }
{ provide: DynamicFormService, useValue: formServiceStub },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: AuthService, useValue: new AuthServiceMock() },
{ provide: RequestService, useValue: requestServiceStub },
{ provide: ObjectCacheService, useValue: objectCacheStub }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ComColFormComponent);
comp = fixture.componentInstance;
comp.formModel = [];
comp.dso = new Community();
fixture.detectChanges();
location = (comp as any).location;
});
describe('onSubmit', () => {
describe('when the dso doesn\'t contain an ID (newly created)', () => {
beforeEach(() => {
spyOn(comp.submitForm, 'emit');
comp.formModel = formModel;
initComponent(new Community());
});
it('should emit the new version of the community', () => {
comp.dso = Object.assign(
new Community(),
{
metadata: {
...titleMD,
...randomMD
}
}
);
it('should initialize the uploadFilesOptions with a placeholder url', () => {
expect(comp.uploadFilesOptions.url.length).toBeGreaterThan(0);
});
comp.onSubmit();
describe('onSubmit', () => {
beforeEach(() => {
spyOn(comp.submitForm, 'emit');
comp.formModel = formModel;
});
expect(comp.submitForm.emit).toHaveBeenCalledWith(
Object.assign(
{},
it('should emit the new version of the community', () => {
comp.dso = Object.assign(
new Community(),
{
metadata: {
...newTitleMD,
...randomMD,
...abstractMD
},
type: Community.type
},
)
);
})
});
...titleMD,
...randomMD
}
}
);
describe('onCancel', () => {
it('should call the back method on the Location service', () => {
comp.onSubmit();
expect(comp.submitForm.emit).toHaveBeenCalledWith(
{
dso: Object.assign(
{},
new Community(),
{
metadata: {
...newTitleMD,
...randomMD,
...abstractMD
},
type: Community.type
},
),
uploader: {},
deleteLogo: false
}
);
})
});
describe('onCancel', () => {
it('should call the back method on the Location service', () => {
comp.onCancel();
expect(locationStub.back).toHaveBeenCalled();
});
});
describe('onCompleteItem', () => {
beforeEach(() => {
spyOn(comp.finish, 'emit');
comp.onCompleteItem();
});
it('should show a success notification', () => {
expect(notificationsService.success).toHaveBeenCalled();
});
it('should emit finish', () => {
expect(comp.finish.emit).toHaveBeenCalled();
});
it('should remove the object\'s cache', () => {
expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled();
expect(objectCacheStub.remove).toHaveBeenCalled();
});
});
describe('onUploadError', () => {
beforeEach(() => {
spyOn(comp.finish, 'emit');
comp.onUploadError();
});
it('should show an error notification', () => {
expect(notificationsService.error).toHaveBeenCalled();
});
it('should emit finish', () => {
expect(comp.finish.emit).toHaveBeenCalled();
});
});
});
describe('when the dso contains an ID (being edited)', () => {
describe('and the dso doesn\'t contain a logo', () => {
beforeEach(() => {
initComponent(Object.assign(new Community(), {
id: 'community-id',
logo: observableOf(new RemoteData(false, false, true, null, undefined))
}));
});
it('should initialize the uploadFilesOptions with the logo\'s endpoint url', () => {
expect(comp.uploadFilesOptions.url).toEqual(logoEndpoint);
});
it('should initialize the uploadFilesOptions with a POST method', () => {
expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.POST);
});
});
describe('and the dso contains a logo', () => {
beforeEach(() => {
initComponent(Object.assign(new Community(), {
id: 'community-id',
logo: observableOf(new RemoteData(false, false, true, null, {}))
}));
});
it('should initialize the uploadFilesOptions with the logo\'s endpoint url', () => {
expect(comp.uploadFilesOptions.url).toEqual(logoEndpoint);
});
it('should initialize the uploadFilesOptions with a PUT method', () => {
expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.PUT);
});
describe('submit with logo marked for deletion', () => {
beforeEach(() => {
comp.markLogoForDeletion = true;
});
describe('when dsoService.deleteLogo returns a successful response', () => {
const response = new RestResponse(true, 200, 'OK');
beforeEach(() => {
spyOn(dsoService, 'deleteLogo').and.returnValue(observableOf(response));
comp.onSubmit();
});
it('should display a success notification', () => {
expect(notificationsService.success).toHaveBeenCalled();
});
});
describe('when dsoService.deleteLogo returns an error response', () => {
const response = new ErrorResponse(new RequestError('errorMessage'));
beforeEach(() => {
spyOn(dsoService, 'deleteLogo').and.returnValue(observableOf(response));
comp.onSubmit();
});
it('should display an error notification', () => {
expect(notificationsService.error).toHaveBeenCalled();
});
});
});
describe('deleteLogo', () => {
beforeEach(() => {
comp.deleteLogo();
fixture.detectChanges();
});
it('should set markLogoForDeletion to true', () => {
expect(comp.markLogoForDeletion).toEqual(true);
});
it('should mark the logo section with a danger alert', () => {
const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger'));
expect(logoSection).toBeTruthy();
});
it('should hide the delete button', () => {
const button = fixture.debugElement.query(By.css('#logo-section .btn-danger'));
expect(button).not.toBeTruthy();
});
it('should show the undo button', () => {
const button = fixture.debugElement.query(By.css('#logo-section .btn-warning'));
expect(button).toBeTruthy();
});
});
describe('undoDeleteLogo', () => {
beforeEach(() => {
comp.markLogoForDeletion = true;
comp.undoDeleteLogo();
fixture.detectChanges();
});
it('should set markLogoForDeletion to false', () => {
expect(comp.markLogoForDeletion).toEqual(false);
});
it('should disable the danger alert on the logo section', () => {
const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger'));
expect(logoSection).not.toBeTruthy();
});
it('should show the delete button', () => {
const button = fixture.debugElement.query(By.css('#logo-section .btn-danger'));
expect(button).toBeTruthy();
});
it('should hide the undo button', () => {
const button = fixture.debugElement.query(By.css('#logo-section .btn-warning'));
expect(button).not.toBeTruthy();
});
});
});
});
function initComponent(dso: Community) {
fixture = TestBed.createComponent(ComColFormComponent);
comp = fixture.componentInstance;
comp.formModel = [];
comp.dso = dso;
(comp as any).type = Community.type;
comp.uploaderComponent = Object.assign({
uploader: {}
});
(comp as any).dsoService = dsoService;
fixture.detectChanges();
location = (comp as any).location;
}
});

View File

@@ -1,17 +1,30 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { Location } from '@angular/common';
import {
DynamicFormService,
DynamicInputModel
} from '@ng-dynamic-forms/core';
import { DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core';
import { FormGroup } from '@angular/forms';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
import { TranslateService } from '@ngx-translate/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models';
import { ResourceType } from '../../../core/shared/resource-type';
import { isNotEmpty } from '../../empty.util';
import { hasValue, isNotEmpty } from '../../empty.util';
import { UploaderOptions } from '../../uploader/uploader-options.model';
import { NotificationsService } from '../../notifications/notifications.service';
import { ComColDataService } from '../../../core/data/comcol-data.service';
import { Subscription } from 'rxjs/internal/Subscription';
import { AuthService } from '../../../core/auth/auth.service';
import { Community } from '../../../core/shared/community.model';
import { Collection } from '../../../core/shared/collection.model';
import { UploaderComponent } from '../../uploader/uploader.component';
import { FileUploader } from 'ng2-file-upload';
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { RemoteData } from '../../../core/data/remote-data';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { RestRequestMethod } from '../../../core/data/rest-request-method';
import { RequestService } from '../../../core/data/request.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
/**
* A form for creating and editing Communities or Collections
@@ -21,7 +34,13 @@ import { Community } from '../../../core/shared/community.model';
styleUrls: ['./comcol-form.component.scss'],
templateUrl: './comcol-form.component.html'
})
export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDestroy {
/**
* The logo uploader component
*/
@ViewChild(UploaderComponent) uploaderComponent: UploaderComponent;
/**
* DSpaceObject that the form represents
*/
@@ -30,7 +49,7 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
/**
* Type of DSpaceObject that the form represents
*/
protected type: ResourceType;
type: ResourceType;
/**
* @type {string} Key prefix used to generate form labels
@@ -53,14 +72,56 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
formGroup: FormGroup;
/**
* Emits DSO when the form is submitted
* @type {EventEmitter<any>}
* The uploader configuration options
* @type {UploaderOptions}
*/
@Output() submitForm: EventEmitter<any> = new EventEmitter();
uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), {
autoUpload: false
});
public constructor(private location: Location,
private formService: DynamicFormService,
private translate: TranslateService) {
/**
* Emits DSO and Uploader when the form is submitted
*/
@Output() submitForm: EventEmitter<{
dso: T,
uploader: FileUploader,
deleteLogo: boolean
}> = new EventEmitter();
/**
* Fires an event when the logo has finished uploading (with or without errors) or was removed
*/
@Output() finish: EventEmitter<any> = new EventEmitter();
/**
* Observable keeping track whether or not the uploader has finished initializing
* Used to start rendering the uploader component
*/
initializedUploaderOptions = new BehaviorSubject(false);
/**
* Is the logo marked to be deleted?
*/
markLogoForDeletion = false;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
/**
* The service used to fetch from or send data to
*/
protected dsoService: ComColDataService<Community | Collection>;
public constructor(protected location: Location,
protected formService: DynamicFormService,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected requestService: RequestService,
protected objectCache: ObjectCacheService) {
}
ngOnInit(): void {
@@ -76,12 +137,55 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
.subscribe(() => {
this.updateFieldTranslations();
});
if (hasValue(this.dso.id)) {
this.subs.push(
observableCombineLatest(
this.dsoService.getLogoEndpoint(this.dso.id),
(this.dso as any).logo
).subscribe(([href, logoRD]: [string, RemoteData<Bitstream>]) => {
this.uploadFilesOptions.url = href;
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
// If the object already contains a logo, send out a PUT request instead of POST for setting a new logo
if (hasValue(logoRD.payload)) {
this.uploadFilesOptions.method = RestRequestMethod.PUT;
}
this.initializedUploaderOptions.next(true);
})
);
} else {
// Set a placeholder URL to not break the uploader component. This will be replaced once the object is created.
this.uploadFilesOptions.url = 'placeholder';
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
this.initializedUploaderOptions.next(true);
}
}
/**
* Checks which new fields were added and sends the updated version of the DSO to the parent component
*/
onSubmit() {
if (this.markLogoForDeletion && hasValue(this.dso.id)) {
this.dsoService.deleteLogo(this.dso).subscribe((response: RestResponse) => {
if (response.isSuccessful) {
this.notificationsService.success(
this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.title'),
this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.content')
);
} else {
const errorResponse = response as ErrorResponse;
this.notificationsService.error(
this.translate.get(this.type.value + '.edit.logo.notifications.delete.error.title'),
errorResponse.errorMessage
);
}
(this.dso as any).logo = undefined;
this.uploadFilesOptions.method = RestRequestMethod.POST;
this.refreshCache();
this.finish.emit();
});
}
const formMetadata = new Object() as MetadataMap;
this.formModel.forEach((fieldModel: DynamicInputModel) => {
const value: MetadataValue = {
@@ -102,7 +206,11 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
},
type: Community.type
});
this.submitForm.emit(updatedDSO);
this.submitForm.emit({
dso: updatedDSO,
uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined,
deleteLogo: this.markLogoForDeletion
});
}
/**
@@ -122,7 +230,59 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
);
}
/**
* Mark the logo to be deleted
* Send out a delete request to remove the logo from the community/collection and display notifications
*/
deleteLogo() {
this.markLogoForDeletion = true;
}
/**
* Undo marking the logo to be deleted
*/
undoDeleteLogo() {
this.markLogoForDeletion = false;
}
/**
* Refresh the object's cache to ensure the latest version
*/
private refreshCache() {
this.requestService.removeByHrefSubstring(this.dso.self);
this.objectCache.remove(this.dso.self);
}
/**
* The request was successful, display a success notification
*/
public onCompleteItem() {
this.refreshCache();
this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success'));
this.finish.emit();
}
/**
* The request was unsuccessful, display an error notification
*/
public onUploadError() {
this.notificationsService.error(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.error'));
this.finish.emit();
}
/**
* Cancel the form and return to the previous page
*/
onCancel() {
this.location.back();
}
/**
* Unsubscribe from open subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
}

View File

@@ -11,11 +11,13 @@ import { RouterTestingModule } from '@angular/router/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { CreateComColPageComponent } from './create-comcol-page.component';
import { DataService } from '../../../core/data/data.service';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$
} from '../../testing/utils';
import { ComColDataService } from '../../../core/data/comcol-data.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../testing/notifications-service-stub';
describe('CreateComColPageComponent', () => {
let comp: CreateComColPageComponent<DSpaceObject>;
@@ -31,6 +33,8 @@ describe('CreateComColPageComponent', () => {
let routeServiceStub;
let routerStub;
const logoEndpoint = 'rest/api/logo/endpoint';
function initializeVars() {
community = Object.assign(new Community(), {
uuid: 'a20da287-e174-466a-9926-f66b9300d347',
@@ -56,8 +60,8 @@ describe('CreateComColPageComponent', () => {
value: community.name
}]
})),
create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity)
create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity),
getLogoEndpoint: () => observableOf(logoEndpoint)
};
routeServiceStub = {
@@ -74,10 +78,11 @@ describe('CreateComColPageComponent', () => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
providers: [
{ provide: DataService, useValue: communityDataServiceStub },
{ provide: ComColDataService, useValue: communityDataServiceStub },
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: Router, useValue: routerStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@@ -86,6 +91,7 @@ describe('CreateComColPageComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(CreateComColPageComponent);
comp = fixture.componentInstance;
(comp as any).type = Community.type;
fixture.detectChanges();
dsoDataService = (comp as any).dsoDataService;
communityDataService = (comp as any).communityDataService;
@@ -95,27 +101,86 @@ describe('CreateComColPageComponent', () => {
describe('onSubmit', () => {
let data;
beforeEach(() => {
data = Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'test'
}]
describe('with an empty queue in the uploader', () => {
beforeEach(() => {
data = {
dso: Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'test'
}]
}),
uploader: {
options: {
url: ''
},
queue: [],
/* tslint:disable:no-empty */
uploadAll: () => {}
/* tslint:enable:no-empty */
}
};
});
it('should navigate when successful', () => {
spyOn(router, 'navigate');
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).toHaveBeenCalled();
});
it('should not navigate on failure', () => {
spyOn(router, 'navigate');
spyOn(dsoDataService, 'create').and.returnValue(createFailedRemoteDataObject$(newCommunity));
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
});
});
it('should navigate when successful', () => {
spyOn(router, 'navigate');
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).toHaveBeenCalled();
});
it('should not navigate on failure', () => {
spyOn(router, 'navigate');
spyOn(dsoDataService, 'create').and.returnValue(createFailedRemoteDataObject$(newCommunity));
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
describe('with at least one item in the uploader\'s queue', () => {
beforeEach(() => {
data = {
dso: Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'test'
}]
}),
uploader: {
options: {
url: ''
},
queue: [
{}
],
/* tslint:disable:no-empty */
uploadAll: () => {}
/* tslint:enable:no-empty */
}
};
});
it('should not navigate', () => {
spyOn(router, 'navigate');
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
});
it('should set the uploader\'s url to the logo\'s endpoint', () => {
comp.onSubmit(data);
fixture.detectChanges();
expect(data.uploader.options.url).toEqual(logoEndpoint);
});
it('should call the uploader\'s uploadAll', () => {
spyOn(data.uploader, 'uploadAll');
comp.onSubmit(data);
fixture.detectChanges();
expect(data.uploader.uploadAll).toHaveBeenCalled();
});
});
});
});

View File

@@ -3,13 +3,17 @@ import { Community } from '../../../core/shared/community.model';
import { CommunityDataService } from '../../../core/data/community-data.service';
import { Observable } from 'rxjs';
import { RouteService } from '../../../core/services/route.service';
import { Router } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { RemoteData } from '../../../core/data/remote-data';
import { isNotEmpty, isNotUndefined } from '../../empty.util';
import { hasValue, isNotEmpty, isNotUndefined } from '../../empty.util';
import { take } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { DataService } from '../../../core/data/data.service';
import { ComColDataService } from '../../../core/data/comcol-data.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { ResourceType } from '../../../core/shared/resource-type';
/**
* Component representing the create page for communities and collections
@@ -34,11 +38,23 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements
*/
public parentRD$: Observable<RemoteData<Community>>;
/**
* The UUID of the newly created object
*/
private newUUID: string;
/**
* The type of the dso
*/
protected type: ResourceType;
public constructor(
protected dsoDataService: DataService<TDomain>,
protected dsoDataService: ComColDataService<TDomain>,
protected parentDataService: CommunityDataService,
protected routeService: RouteService,
protected router: Router
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
}
@@ -53,20 +69,40 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements
}
/**
* @param {TDomain} dso The updated version of the DSO
* Creates a new DSO based on the submitted user data and navigates to the new object's home page
* @param event The event returned by the community/collection form. Contains the new dso and logo uploader
*/
onSubmit(dso: TDomain) {
onSubmit(event) {
const dso = event.dso;
const uploader = event.uploader;
this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => {
this.dsoDataService.create(dso, uuid)
.pipe(getSucceededRemoteData())
.subscribe((dsoRD: RemoteData<TDomain>) => {
if (isNotUndefined(dsoRD)) {
const newUUID = dsoRD.payload.uuid;
this.router.navigate([this.frontendURL + newUUID]);
this.newUUID = dsoRD.payload.uuid;
if (uploader.queue.length > 0) {
this.dsoDataService.getLogoEndpoint(this.newUUID).pipe(take(1)).subscribe((href: string) => {
uploader.options.url = href;
uploader.uploadAll();
});
} else {
this.navigateToNewPage();
}
this.notificationsService.success(null, this.translate.get(this.type.value + '.create.notifications.success'));
}
});
});
}
/**
* Navigate to the page of the newly created object
*/
navigateToNewPage() {
if (hasValue(this.newUUID)) {
this.router.navigate([this.frontendURL + this.newUUID]);
}
}
}

View File

@@ -12,6 +12,10 @@ import { RouterTestingModule } from '@angular/router/testing';
import { DataService } from '../../../../core/data/data.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComcolMetadataComponent } from './comcol-metadata.component';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../testing/utils';
import { ComColDataService } from '../../../../core/data/comcol-data.service';
import { NotificationsServiceStub } from '../../../testing/notifications-service-stub';
import { NotificationsService } from '../../../notifications/notifications.service';
describe('ComColMetadataComponent', () => {
let comp: ComcolMetadataComponent<DSpaceObject>;
@@ -25,6 +29,8 @@ describe('ComColMetadataComponent', () => {
let routerStub;
let routeStub;
const logoEndpoint = 'rest/api/logo/endpoint';
function initializeVars() {
community = Object.assign(new Community(), {
uuid: 'a20da287-e174-466a-9926-f66b9300d347',
@@ -43,8 +49,8 @@ describe('ComColMetadataComponent', () => {
});
communityDataServiceStub = {
update: (com, uuid?) => observableOf(new RemoteData(false, false, true, undefined, newCommunity))
update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity),
getLogoEndpoint: () => observableOf(logoEndpoint)
};
routerStub = {
@@ -53,7 +59,9 @@ describe('ComColMetadataComponent', () => {
routeStub = {
parent: {
data: observableOf(community)
data: observableOf({
dso: new RemoteData(false, false, true, null, community)
})
}
};
@@ -64,9 +72,10 @@ describe('ComColMetadataComponent', () => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
providers: [
{ provide: DataService, useValue: communityDataServiceStub },
{ provide: ComColDataService, useValue: communityDataServiceStub },
{ provide: Router, useValue: routerStub },
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@@ -75,6 +84,7 @@ describe('ComColMetadataComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ComcolMetadataComponent);
comp = fixture.componentInstance;
(comp as any).type = Community.type;
fixture.detectChanges();
dsoDataService = (comp as any).dsoDataService;
router = (comp as any).router;
@@ -82,27 +92,98 @@ describe('ComColMetadataComponent', () => {
describe('onSubmit', () => {
let data;
beforeEach(() => {
data = Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'test'
}]
describe('with an empty queue in the uploader', () => {
beforeEach(() => {
data = {
dso: Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'test'
}]
}),
uploader: {
options: {
url: ''
},
queue: [],
/* tslint:disable:no-empty */
uploadAll: () => {}
/* tslint:enable:no-empty */
}
}
});
it('should navigate when successful', () => {
spyOn(router, 'navigate');
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).toHaveBeenCalled();
});
it('should not navigate on failure', () => {
spyOn(router, 'navigate');
spyOn(dsoDataService, 'update').and.returnValue(createFailedRemoteDataObject$(newCommunity));
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
});
});
it('should navigate when successful', () => {
spyOn(router, 'navigate');
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).toHaveBeenCalled();
});
it('should not navigate on failure', () => {
spyOn(router, 'navigate');
spyOn(dsoDataService, 'update').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity)));
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
describe('with at least one item in the uploader\'s queue', () => {
beforeEach(() => {
data = {
dso: Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'test'
}]
}),
uploader: {
options: {
url: ''
},
queue: [
{}
],
/* tslint:disable:no-empty */
uploadAll: () => {}
/* tslint:enable:no-empty */
}
}
});
it('should not navigate', () => {
spyOn(router, 'navigate');
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
});
it('should set the uploader\'s url to the logo\'s endpoint', () => {
comp.onSubmit(data);
fixture.detectChanges();
expect(data.uploader.options.url).toEqual(logoEndpoint);
});
it('should call the uploader\'s uploadAll', () => {
spyOn(data.uploader, 'uploadAll');
comp.onSubmit(data);
fixture.detectChanges();
expect(data.uploader.uploadAll).toHaveBeenCalled();
});
});
});
describe('navigateToHomePage', () => {
beforeEach(() => {
spyOn(router, 'navigate');
comp.navigateToHomePage();
});
it('should navigate', () => {
expect(router.navigate).toHaveBeenCalled();
});
});
});

View File

@@ -3,10 +3,14 @@ import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../../core/data/remote-data';
import { ActivatedRoute, Router } from '@angular/router';
import { first, map } from 'rxjs/operators';
import { first, map, take } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { isNotUndefined } from '../../../empty.util';
import { hasValue, isNotUndefined } from '../../../empty.util';
import { DataService } from '../../../../core/data/data.service';
import { ResourceType } from '../../../../core/shared/resource-type';
import { ComColDataService } from '../../../../core/data/comcol-data.service';
import { NotificationsService } from '../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'ds-comcol-metadata',
@@ -17,13 +21,22 @@ export class ComcolMetadataComponent<TDomain extends DSpaceObject> implements On
* Frontend endpoint for this type of DSO
*/
protected frontendURL: string;
/**
* The initial DSO object
*/
public dsoRD$: Observable<RemoteData<TDomain>>;
/**
* The type of the dso
*/
protected type: ResourceType;
public constructor(
protected dsoDataService: DataService<TDomain>,
protected dsoDataService: ComColDataService<TDomain>,
protected router: Router,
protected route: ActivatedRoute
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected translate: TranslateService
) {
}
@@ -32,17 +45,41 @@ export class ComcolMetadataComponent<TDomain extends DSpaceObject> implements On
}
/**
* @param {TDomain} dso The updated version of the DSO
* Updates an existing DSO based on the submitted user data and navigates to the edited object's home page
* @param event The event returned by the community/collection form. Contains the new dso and logo uploader
*/
onSubmit(dso: TDomain) {
onSubmit(event) {
const dso = event.dso;
const uploader = event.uploader;
const deleteLogo = event.deleteLogo;
this.dsoDataService.update(dso)
.pipe(getSucceededRemoteData())
.subscribe((dsoRD: RemoteData<TDomain>) => {
if (isNotUndefined(dsoRD)) {
const newUUID = dsoRD.payload.uuid;
this.router.navigate([this.frontendURL + newUUID]);
if (hasValue(uploader) && uploader.queue.length > 0) {
this.dsoDataService.getLogoEndpoint(newUUID).pipe(take(1)).subscribe((href: string) => {
uploader.options.url = href;
uploader.uploadAll();
});
} else if (!deleteLogo) {
this.router.navigate([this.frontendURL + newUUID]);
}
this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.notifications.success'));
}
});
}
/**
* Navigate to the home page of the object
*/
navigateToHomePage() {
this.dsoRD$.pipe(
getSucceededRemoteData(),
take(1)
).subscribe((dsoRD: RemoteData<TDomain>) => {
this.router.navigate([this.frontendURL + dsoRD.payload.id]);
});
}
}

View File

@@ -16,7 +16,7 @@
<div class="mb-4">
<router-outlet></router-outlet>
</div>
<a [routerLink]="getPageUrl((dsoRD$ | async)?.payload)" class="btn btn-outline-secondary">{{ type + '.edit.return' | translate }}</a>
<a *ngIf="!hideReturnButton" [routerLink]="getPageUrl((dsoRD$ | async)?.payload)" class="btn btn-outline-secondary">{{ type + '.edit.return' | translate }}</a>
</div>
</div>
</div>

View File

@@ -39,7 +39,14 @@ describe('EditComColPageComponent', () => {
dso: community
}),
routeConfig: {
children: []
children: [
{
path: 'mockUrl',
data: {
hideReturnButton: false
}
}
]
},
snapshot: {
firstChild: {

View File

@@ -34,12 +34,19 @@ export class EditComColPageComponent<TDomain extends DSpaceObject> implements On
*/
public dsoRD$: Observable<RemoteData<TDomain>>;
/**
* Hide the default return button?
*/
public hideReturnButton: boolean;
public constructor(
protected router: Router,
protected route: ActivatedRoute
) {
this.router.events.subscribe(() => {
this.currentPage = this.route.snapshot.firstChild.routeConfig.path;
this.hideReturnButton = this.route.routeConfig.children
.find((child: any) => child.path === this.currentPage).data.hideReturnButton;
});
}

View File

@@ -3,4 +3,7 @@ export class AuthServiceMock {
public checksAuthenticationToken() {
return
}
public buildAuthHeader() {
return 'auth-header';
}
}

View File

@@ -130,8 +130,6 @@ import { RoleDirective } from './roles/role.directive';
import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component';
import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component';
import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component';
import { AbstractTrackableComponent } from './trackable/abstract-trackable.component';
import { ComcolMetadataComponent } from './comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component';
import { DsDynamicLookupRelationModalComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
import { SearchResultsComponent } from './search/search-results/search-results.component';
import { SearchSidebarComponent } from './search/search-sidebar/search-sidebar.component';
@@ -156,6 +154,8 @@ import { DsDynamicDisabledComponent } from './form/builder/ds-dynamic-form-ui/mo
import { DsDynamicLookupRelationSearchTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component';
import { DsDynamicLookupRelationSelectionTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component';
import { PageSizeSelectorComponent } from './page-size-selector/page-size-selector.component';
import { AbstractTrackableComponent } from './trackable/abstract-trackable.component';
import { ComcolMetadataComponent } from './comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component';
import { ItemSelectComponent } from './object-select/item-select/item-select.component';
import { CollectionSelectComponent } from './object-select/collection-select/collection-select.component';
import { FilterInputSuggestionsComponent } from './input-suggestions/filter-suggestions/filter-input-suggestions.component';

View File

@@ -1,3 +1,4 @@
import { RestRequestMethod } from '../../core/data/rest-request-method';
export class UploaderOptions {
/**
@@ -9,5 +10,15 @@ export class UploaderOptions {
disableMultipart = false;
itemAlias: string;
itemAlias: string = null;
/**
* Automatically send out an upload request when adding files
*/
autoUpload = true;
/**
* The request method to use for the file upload request
*/
method: RestRequestMethod = RestRequestMethod.POST;
}

View File

@@ -19,23 +19,24 @@
(fileOver)="fileOverBase($event)"
class="well ds-base-drop-zone mt-1 mb-3 text-muted">
<p class="text-center m-0 p-0 d-flex justify-content-center align-items-center" *ngIf="uploader?.queue?.length === 0">
<span><i class="fas fa-cloud-upload" aria-hidden="true"></i> {{dropMsg | translate}} {{'uploader.or' | translate}}
<label class="btn btn-link m-0 p-0">
<input class="d-none" type="file" ng2FileSelect [uploader]="uploader" multiple />
{{'uploader.browse' | translate}}
</label>
</span>
<span><i class="fas fa-cloud-upload" aria-hidden="true"></i> {{dropMsg | translate}} {{'uploader.or' | translate}}</span>
<label class="btn btn-link m-0 p-0 ml-1">
<input class="d-none" type="file" ng2FileSelect [uploader]="uploader" multiple />
{{'uploader.browse' | translate}}
</label>
</p>
<div *ngIf="(isOverBaseDropZone | async) || uploader?.queue?.length !== 0">
<div class="m-1">
<div class="upload-item-top">
<span class="filename">{{'uploader.queue-length' | translate}}: {{ uploader?.queue?.length }} | {{ uploader?.queue[0]?.file.name }}</span>
<span class="filename">
<span *ngIf="!uploader.options.disableMultipart">{{'uploader.queue-length' | translate}}: {{ uploader?.queue?.length }} | </span>{{ uploader?.queue[0]?.file.name }}
</span>
<div class="btn-group btn-group-sm float-right" role="group">
<button type="button" class="btn btn-danger" (click)="uploader.clearQueue()" [disabled]="!uploader.queue.length">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
</div>
<span *ngIf="uploader.progress < 100" class="float-right mr-3">{{ uploader.progress }}%</span>
<span *ngIf="uploader.progress < 100 && !(uploader.progress === 0 && !uploader.options.autoUpload)" class="float-right mr-3">{{ uploader.progress }}%</span>
<span *ngIf="uploader.progress === 100" class="float-right mr-3">{{'uploader.processing' | translate}}...</span>
</div>
<div class="ds-base-drop-zone-progress clearfix mt-2">

View File

@@ -64,12 +64,12 @@ describe('Chips component', () => {
template: ``
})
class TestComponent {
public uploadFilesOptions: UploaderOptions = {
public uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null
};
});
/* tslint:disable:no-empty */
public onBeforeUpload = () => {

View File

@@ -95,7 +95,8 @@ export class UploaderComponent {
disableMultipart: this.uploadFilesOptions.disableMultipart,
itemAlias: this.uploadFilesOptions.itemAlias,
removeAfterUpload: true,
autoUpload: true
autoUpload: this.uploadFilesOptions.autoUpload,
method: this.uploadFilesOptions.method
});
if (isUndefined(this.enableDragOverDocument)) {
@@ -117,7 +118,10 @@ export class UploaderComponent {
if (isUndefined(this.onBeforeUpload)) {
this.onBeforeUpload = () => {return};
}
this.uploader.onBeforeUploadItem = () => {
this.uploader.onBeforeUploadItem = (item) => {
if (item.url !== this.uploader.options.url) {
item.url = this.uploader.options.url;
}
this.onBeforeUpload();
this.isOverDocumentDropZone = observableOf(false);

View File

@@ -77,12 +77,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
* The uploader configuration options
* @type {UploaderOptions}
*/
public uploadFilesOptions: UploaderOptions = {
url: '',
authToken: null,
disableMultipart: false,
itemAlias: null
};
public uploadFilesOptions: UploaderOptions = new UploaderOptions();
/**
* A boolean representing if component is active

View File

@@ -28,6 +28,7 @@ import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testin
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
import { SharedModule } from '../../../shared/shared.module';
import { createTestComponent } from '../../../shared/testing/utils';
import { UploaderOptions } from '../../../shared/uploader/uploader-options.model';
describe('SubmissionUploadFilesComponent Component', () => {
@@ -112,12 +113,12 @@ describe('SubmissionUploadFilesComponent Component', () => {
comp.submissionId = submissionId;
comp.collectionId = collectionId;
comp.sectionId = 'upload';
comp.uploadFilesOptions = {
comp.uploadFilesOptions = Object.assign(new UploaderOptions(),{
url: '',
authToken: null,
disableMultipart: false,
itemAlias: null
};
});
});
@@ -208,11 +209,11 @@ class TestComponent {
submissionId = mockSubmissionId;
collectionId = mockSubmissionCollectionId;
sectionId = 'upload';
uploadFilesOptions = {
uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: '',
authToken: null,
disableMultipart: false,
itemAlias: null
};
});
}