diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 6570d5bf3a..cc6d49814b 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -130,6 +130,10 @@ "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", "collection.edit.delete": "Delete this collection", "collection.edit.head": "Edit Collection", + "collection.edit.logo.label": "Collection logo", + "collection.edit.logo.notifications.error": "Uploading Collection logo failed. Please verify the content before retrying.", + "collection.edit.logo.notifications.success": "Upload Collection logo successful.", + "collection.edit.logo.upload": "Drop a Collection Logo to upload", "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", "collection.form.errors.title.required": "Please enter a collection name", @@ -153,6 +157,10 @@ "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", "community.edit.delete": "Delete this community", "community.edit.head": "Edit Community", + "community.edit.logo.label": "Community logo", + "community.edit.logo.notifications.error": "Uploading Community logo failed. Please verify the content before retrying.", + "community.edit.logo.notifications.success": "Upload Community logo successful.", + "community.edit.logo.upload": "Drop a Community Logo to upload", "community.form.abstract": "Short Description", "community.form.description": "Introductory text (HTML)", "community.form.errors.title.required": "Please enter a community name", diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts index 21b494f41f..e3ca07a2ad 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -1,9 +1,14 @@ 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'; /** * Form used for creating and editing collections @@ -19,6 +24,26 @@ export class CollectionFormComponent extends ComColFormComponent { */ @Input() dso: Collection = new Collection(); + /** + * i18n key for the logo's label + */ + protected logoLabelMsg = 'collection.edit.logo.label'; + + /** + * i18n key for the logo's drop message + */ + protected logoDropMsg = 'collection.edit.logo.upload'; + + /** + * i18n key for the logo's upload success message + */ + protected logoSuccessMsg = 'collection.edit.logo.notifications.success'; + + /** + * i18n key for the logo's upload error message + */ + protected logoErrorMsg = 'collection.edit.logo.notifications.error'; + /** * @type {Collection.type} This is a collection-type form */ @@ -65,4 +90,13 @@ export class CollectionFormComponent extends ComColFormComponent { name: 'dc.description.provenance', }), ]; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected dsoService: CommunityDataService) { + super(location, formService, translate, notificationsService, authService); + } } diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html index b3f4361bc6..dc5c5b186a 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -4,5 +4,5 @@

{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}

- + diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html index c389c681ce..b4429d0f7c 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html @@ -2,7 +2,9 @@
- + {{'collection.edit.delete' | translate}} diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts index 17d601e251..29149f4d01 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -1,9 +1,14 @@ 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'; /** * Form used for creating and editing communities @@ -19,6 +24,26 @@ export class CommunityFormComponent extends ComColFormComponent { */ @Input() dso: Community = new Community(); + /** + * i18n key for the logo's label + */ + protected logoLabelMsg = 'community.edit.logo.label'; + + /** + * i18n key for the logo's drop message + */ + protected logoDropMsg = 'community.edit.logo.upload'; + + /** + * i18n key for the logo's upload success message + */ + protected logoSuccessMsg = 'community.edit.logo.notifications.success'; + + /** + * i18n key for the logo's upload error message + */ + protected logoErrorMsg = 'community.edit.logo.notifications.error'; + /** * @type {Community.type} This is a community-type form */ @@ -57,4 +82,13 @@ export class CommunityFormComponent extends ComColFormComponent { name: 'dc.description.tableofcontents', }), ]; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected dsoService: CommunityDataService) { + super(location, formService, translate, notificationsService, authService); + } } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index 55a080d2a1..c1b0cf5971 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -7,5 +7,5 @@
- + diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.html b/src/app/+community-page/edit-community-page/edit-community-page.component.html index cedb771c14..ba3a3ce32b 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.html +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.html @@ -3,7 +3,8 @@
+ [dso]="(dsoRD$ | async)?.payload" + (finishUpload)="navigateToHomePage()"> {{'community.edit.delete' | translate}} diff --git a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts index 938a1ec899..26ed4110b3 100644 --- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts +++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts @@ -34,7 +34,8 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { url: '', authToken: null, disableMultipart: false, - itemAlias: null + itemAlias: null, + autoUpload: true }; /** diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 68eb3e4880..42c414a8a7 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,4 +1,4 @@ -import { distinctUntilChanged, filter, map, mergeMap, share, take, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, mergeMap, share, switchMap, take, tap } from 'rxjs/operators'; import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; @@ -57,4 +57,14 @@ export abstract class ComColDataService extends DataS return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged(), share()); } } + + /** + * Get the endpoint for the community or collection's logo + * @param id The community or collection's ID + */ + public getLogoEndpoint(id: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((href: string) => this.halService.getEndpoint('logo', `${href}/${id}`)) + ) + } } diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index a93d54db64..117cc074ca 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -43,8 +43,8 @@ export class HALEndpointService { ); } - public getEndpoint(linkPath: string): Observable { - return this.getEndpointAt(this.getRootHref(), ...linkPath.split('/')); + public getEndpoint(linkPath: string, startHref?: string): Observable { + return this.getEndpointAt(startHref || this.getRootHref(), ...linkPath.split('/')); } /** diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html index 6c67937063..8e1e280b22 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html @@ -1,3 +1,14 @@ +
+ + + +
diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index 8d1d5c1dca..7f59b73c6a 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -1,4 +1,4 @@ -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, @@ -10,8 +10,16 @@ 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, isUndefined } 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'; /** * A form for creating and editing Communities or Collections @@ -21,12 +29,38 @@ import { Community } from '../../../core/shared/community.model'; styleUrls: ['./comcol-form.component.scss'], templateUrl: './comcol-form.component.html' }) -export class ComColFormComponent implements OnInit { +export class ComColFormComponent implements OnInit, OnDestroy { + + /** + * The logo uploader component + */ + @ViewChild(UploaderComponent) uploaderComponent: UploaderComponent; + /** * DSpaceObject that the form represents */ @Input() dso: T; + /** + * i18n key for the logo's label + */ + protected logoLabelMsg: string; + + /** + * i18n key for the logo's drop message + */ + protected logoDropMsg: string; + + /** + * i18n key for the logo's upload success message + */ + protected logoSuccessMsg: string; + + /** + * i18n key for the logo's upload error message + */ + protected logoErrorMsg: string; + /** * Type of DSpaceObject that the form represents */ @@ -53,14 +87,46 @@ export class ComColFormComponent implements OnInit { formGroup: FormGroup; /** - * Emits DSO when the form is submitted - * @type {EventEmitter} + * The uploader configuration options + * @type {UploaderOptions} */ - @Output() submitForm: EventEmitter = new EventEmitter(); + uploadFilesOptions: UploaderOptions = { + url: '', + authToken: null, + disableMultipart: true, + itemAlias: null, + 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 + }> = new EventEmitter(); + + /** + * Fires an event when the logo has finished uploading (with or without errors) + */ + @Output() finishUpload: EventEmitter = new EventEmitter(); + + /** + * 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; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService) { } ngOnInit(): void { @@ -76,6 +142,19 @@ export class ComColFormComponent implements OnInit { .subscribe(() => { this.updateFieldTranslations(); }); + + if (hasValue(this.dso.id)) { + this.subs.push( + this.dsoService.getLogoEndpoint(this.dso.id).subscribe((href: string) => { + this.uploadFilesOptions.url = href; + this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); + }) + ); + } 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(); + } } /** @@ -102,7 +181,10 @@ export class ComColFormComponent implements OnInit { }, type: Community.type }); - this.submitForm.emit(updatedDSO); + this.submitForm.emit({ + dso: updatedDSO, + uploader: this.uploaderComponent.uploader + }); } /** @@ -122,7 +204,32 @@ export class ComColFormComponent implements OnInit { ); } + /** + * The request was successful, display a success notification + */ + public onCompleteItem() { + this.notificationsService.success(null, this.translate.get(this.logoSuccessMsg)); + this.finishUpload.emit(); + } + + /** + * The request was unsuccessful, display an error notification + */ + public onUploadError() { + this.notificationsService.error(null, this.translate.get(this.logoErrorMsg)); + this.finishUpload.emit(); + } + onCancel() { this.location.back(); } + + /** + * Unsubscribe from open subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } } diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index e07f2a5a0a..baa05ac5e7 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -5,11 +5,12 @@ import { Observable } from 'rxjs'; import { RouteService } from '../../../core/services/route.service'; import { 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'; /** * Component representing the create page for communities and collections @@ -34,8 +35,13 @@ export class CreateComColPageComponent implements */ public parentRD$: Observable>; + /** + * The UUID of the newly created object + */ + private newUUID: string; + public constructor( - protected dsoDataService: DataService, + protected dsoDataService: ComColDataService, protected parentDataService: CommunityDataService, protected routeService: RouteService, protected router: Router @@ -53,20 +59,39 @@ export class CreateComColPageComponent 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) => { 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(); + } } }); }); } + /** + * Navigate to the page of the newly created object + */ + navigateToNewPage() { + if (hasValue(this.newUUID)) { + this.router.navigate([this.frontendURL + this.newUUID]); + } + } + } diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts index 24181b5e61..ec4a7fd745 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts @@ -3,10 +3,11 @@ import { Observable } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../../core/data/remote-data'; import { isNotUndefined } from '../../empty.util'; -import { first, map } from 'rxjs/operators'; +import { first, map, take } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../../core/shared/operators'; import { DataService } from '../../../core/data/data.service'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { ComColDataService } from '../../../core/data/comcol-data.service'; /** * Component representing the edit page for communities and collections @@ -26,7 +27,7 @@ export class EditComColPageComponent implements On public dsoRD$: Observable>; public constructor( - protected dsoDataService: DataService, + protected dsoDataService: ComColDataService, protected router: Router, protected route: ActivatedRoute ) { @@ -37,17 +38,39 @@ export class EditComColPageComponent 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; + this.dsoDataService.update(dso) .pipe(getSucceededRemoteData()) .subscribe((dsoRD: RemoteData) => { if (isNotUndefined(dsoRD)) { const newUUID = dsoRD.payload.uuid; - this.router.navigate([this.frontendURL + newUUID]); + if (uploader.queue.length > 0) { + this.dsoDataService.getLogoEndpoint(newUUID).pipe(take(1)).subscribe((href: string) => { + uploader.options.url = href; + uploader.uploadAll(); + }); + } else { + this.router.navigate([this.frontendURL + newUUID]); + } } }); } + + /** + * Navigate to the home page of the object + */ + navigateToHomePage() { + this.dsoRD$.pipe( + getSucceededRemoteData(), + take(1) + ).subscribe((dsoRD: RemoteData) => { + this.router.navigate([this.frontendURL + dsoRD.payload.id]); + }); + } } diff --git a/src/app/shared/uploader/uploader-options.model.ts b/src/app/shared/uploader/uploader-options.model.ts index 0bd6412b17..fa77a36d11 100644 --- a/src/app/shared/uploader/uploader-options.model.ts +++ b/src/app/shared/uploader/uploader-options.model.ts @@ -10,4 +10,6 @@ export class UploaderOptions { disableMultipart = false; itemAlias: string; + + autoUpload = true; } diff --git a/src/app/shared/uploader/uploader.component.html b/src/app/shared/uploader/uploader.component.html index 9d994313c6..a3181be21c 100644 --- a/src/app/shared/uploader/uploader.component.html +++ b/src/app/shared/uploader/uploader.component.html @@ -29,13 +29,15 @@
- {{'uploader.queue-length' | translate}}: {{ uploader?.queue?.length }} | {{ uploader?.queue[0]?.file.name }} + + {{'uploader.queue-length' | translate}}: {{ uploader?.queue?.length }} | {{ uploader?.queue[0]?.file.name }} +
- {{ uploader.progress }}% + {{ uploader.progress }}% {{'uploader.processing' | translate}}...
diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/uploader/uploader.component.ts index ad52f4a93f..794b5cb4b3 100644 --- a/src/app/shared/uploader/uploader.component.ts +++ b/src/app/shared/uploader/uploader.component.ts @@ -95,7 +95,7 @@ export class UploaderComponent { disableMultipart: this.uploadFilesOptions.disableMultipart, itemAlias: this.uploadFilesOptions.itemAlias, removeAfterUpload: true, - autoUpload: true + autoUpload: this.uploadFilesOptions.autoUpload }); if (isUndefined(this.enableDragOverDocument)) { @@ -117,7 +117,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); diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index b592972839..7ed7bec04b 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -81,7 +81,8 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { url: '', authToken: null, disableMultipart: false, - itemAlias: null + itemAlias: null, + autoUpload: true }; /**