1
0

64387: Upload bitstream component + Edit bitstream placeholder

This commit is contained in:
Kristof De Langhe
2019-08-16 13:35:04 +02:00
parent d1dbe49333
commit 7bd6c9acab
14 changed files with 327 additions and 17 deletions

View File

@@ -51,6 +51,8 @@
"admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry",
"auth.errors.invalid-user": "Invalid email address or password.",
"auth.messages.expired": "Your session has expired. Please log in again.",
"bitstream.edit.title": "Edit bitstream",
"bitstream.edit.bitstream": "Bitstream: ",
"browse.comcol.by.author": "By Author",
"browse.comcol.by.dateissued": "By Issue Date",
"browse.comcol.by.subject": "By Subject",
@@ -177,6 +179,9 @@
"home.top-level-communities.help": "Select a community to browse its collections.",
"item.bitstreams.upload.bundle-name": "Bundle Name",
"item.bitstreams.upload.drop-message": "Drop a file to upload",
"item.bitstreams.upload.failed": "Upload failed. Please verify the content before retrying.",
"item.bitstreams.upload.item": "Item: ",
"item.bitstreams.upload.title": "Upload bitstream",
"item.edit.delete.cancel": "Cancel",
"item.edit.delete.confirm": "Delete",
"item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.",

View File

@@ -0,0 +1,27 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { BitstreamPageResolver } from './bitstream-page.resolver';
const EDIT_BITSTREAM_PATH = ':id/edit';
@NgModule({
imports: [
RouterModule.forChild([
{
path: EDIT_BITSTREAM_PATH,
component: EditBitstreamPageComponent,
resolve: {
bitstream: BitstreamPageResolver
},
canActivate: [AuthenticatedGuard]
}
])
],
providers: [
BitstreamPageResolver,
]
})
export class BitstreamPageRoutingModule {
}

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '../shared/shared.module';
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
BitstreamPageRoutingModule
],
declarations: [
EditBitstreamPageComponent
]
})
export class BitstreamPageModule {
}

View File

@@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { RemoteData } from '../core/data/remote-data';
import { Observable } from 'rxjs/internal/Observable';
import { find } from 'rxjs/operators';
import { hasValue } from '../shared/empty.util';
import { Bitstream } from '../core/shared/bitstream.model';
import { BitstreamDataService } from '../core/data/bitstream-data.service';
/**
* This class represents a resolver that requests a specific bitstream before the route is activated
*/
@Injectable()
export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> {
constructor(private bitstreamService: BitstreamDataService) {
}
/**
* Method for resolving a bitstream based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> Emits the found bitstream based on the parameters in the current route,
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Bitstream>> {
return this.bitstreamService.findById(route.params.id)
.pipe(
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
);
}
}

View File

@@ -0,0 +1,13 @@
<div class="container">
<div class="row">
<div class="col-12 mb-4">
<h2>{{'bitstream.edit.title' | translate}}</h2>
<ng-container *ngVar="(bitstreamRD$ | async)?.payload as bitstream">
<div *ngIf="bitstream">
<span class="font-weight-bold">{{'bitstream.edit.bitstream' | translate}}</span>
<span>{{bitstream.name}}</span>
</div>
</ng-container>
</div>
</div>
</div>

View File

@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../core/data/remote-data';
import { Bitstream } from '../../core/shared/bitstream.model';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';
@Component({
selector: 'ds-edit-bitstream-page',
templateUrl: './edit-bitstream-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
/**
* Page component for editing a bitstream
*/
export class EditBitstreamPageComponent implements OnInit {
/**
* The bitstream to edit
*/
bitstreamRD$: Observable<RemoteData<Bitstream>>;
constructor(private route: ActivatedRoute) {
}
ngOnInit(): void {
this.bitstreamRD$ = this.route.data.pipe(map((data) => data.bitstream));
}
}

View File

@@ -1,14 +1,28 @@
<div class="container">
<div class="row">
<div class="form-row col-12">
<ng-container *ngVar="(bundleName$ | async) as bundleName">
<label for="bundleName">{{"item.bitstreams.upload.bundle-name" | translate}}</label>
<input id="bundleName"
[value]="bundleName ? bundleName : 'ORIGINAL'"
type="text"
class="form-control" />
<div class="col-12 mb-4">
<h2>{{'item.bitstreams.upload.title' | translate}}</h2>
<ng-container *ngVar="(itemRD$ | async)?.payload as item">
<div *ngIf="item">
<span class="font-weight-bold">{{'item.bitstreams.upload.item' | translate}}</span>
<span>{{item.name}}</span>
</div>
</ng-container>
<ds-uploader [dropMsg]="'item.bitstreams.upload.drop-message' | translate"></ds-uploader>
</div>
<div class="col-12">
<label for="bundleName" class="font-weight-bold">{{'item.bitstreams.upload.bundle-name' | translate}}</label>
<input id="bundleName"
[(ngModel)]="uploadProperties.bundleName"
type="text"
class="form-control" />
<ds-uploader class="w-100"
[dropMsg]="'item.bitstreams.upload.drop-message'"
[dropOverDocumentMsg]="'item.bitstreams.upload.drop-message'"
[enableDragOverDocument]="true"
[uploadFilesOptions]="uploadFilesOptions"
[uploadProperties]="uploadProperties"
(onCompleteItem)="onCompleteItem($event)"
(onUploadError)="onUploadError()"></ds-uploader>
</div>
</div>
</div>

View File

@@ -1,9 +1,18 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model';
import { map } from 'rxjs/operators';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { UploaderOptions } from '../../../shared/uploader/uploader-options.model';
import { Subscription } from 'rxjs/internal/Subscription';
import { hasValue, hasValueOperator } from '../../../shared/empty.util';
import { ItemDataService } from '../../../core/data/item-data.service';
import { AuthService } from '../../../core/auth/auth.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { UploaderProperties } from '../../../shared/uploader/uploader-properties.model';
import { getBitstreamModulePath } from '../../../app-routing.module';
@Component({
selector: 'ds-upload-bitstream',
@@ -13,7 +22,7 @@ import { ActivatedRoute, Router } from '@angular/router';
/**
* Page component for uploading a bitstream to an item
*/
export class UploadBitstreamComponent implements OnInit {
export class UploadBitstreamComponent implements OnInit, OnDestroy {
/**
* The item to upload a bitstream to
@@ -21,17 +30,82 @@ export class UploadBitstreamComponent implements OnInit {
itemRD$: Observable<RemoteData<Item>>;
/**
* The name of the bundle to add the bitstream to
* Defaults to ORIGINAL
* The uploader configuration options
* @type {UploaderOptions}
*/
bundleName$: Observable<string>;
uploadFilesOptions: UploaderOptions = {
url: '',
authToken: null,
disableMultipart: false,
itemAlias: null
};
constructor(private route: ActivatedRoute) {
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
subs: Subscription[] = [];
/**
* Properties to send with the upload request
*/
uploadProperties = Object.assign(new UploaderProperties(), {
bundleName: 'ORIGINAL'
});
constructor(protected route: ActivatedRoute,
protected router: Router,
protected itemService: ItemDataService,
protected authService: AuthService,
protected notificationsService: NotificationsService,
protected translate: TranslateService) {
}
ngOnInit(): void {
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
this.bundleName$ = this.route.queryParams.pipe(map((params) => params.bundleName));
this.subs.push(
this.route.queryParams.pipe(
map((params) => params.bundleName),
hasValueOperator(),
distinctUntilChanged()
).subscribe((bundleName: string) => {
this.uploadProperties.bundleName = bundleName;
})
);
this.subs.push(
this.itemRD$.pipe(
map((itemRD: RemoteData<Item>) => itemRD.payload),
switchMap((item: Item) => this.itemService.getBitstreamsEndpoint(item.id)),
distinctUntilChanged()
).subscribe((url: string) => {
this.uploadFilesOptions.url = url;
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
})
);
}
/**
* The request was successful, redirect the user to the new bitstream's edit page
* @param bitstream
*/
public onCompleteItem(bitstream) {
this.router.navigate([getBitstreamModulePath(), bitstream.id, 'edit']);
}
/**
* The request was unsuccessful, display an error notification
*/
public onUploadError() {
this.notificationsService.error(null, this.translate.get('item.bitstreams.upload.failed'));
}
/**
* Unsubscribe from all open subscriptions when the component is destroyed
*/
ngOnDestroy(): void {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
}

View File

@@ -16,6 +16,10 @@ const COMMUNITY_MODULE_PATH = 'communities';
export function getCommunityModulePath() {
return `/${COMMUNITY_MODULE_PATH}`;
}
const BITSTREAM_MODULE_PATH = 'bitstreams';
export function getBitstreamModulePath() {
return `/${BITSTREAM_MODULE_PATH}`;
}
@NgModule({
imports: [
RouterModule.forRoot([
@@ -24,6 +28,7 @@ export function getCommunityModulePath() {
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: BITSTREAM_MODULE_PATH, loadChildren: './+bitstream-page/bitstream-page.module#BitstreamPageModule' },
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },

View File

@@ -117,6 +117,7 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic
import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model';
import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
import { BrowseDefinition } from './shared/browse-definition.model';
import { BitstreamDataService } from './data/bitstream-data.service';
const IMPORTS = [
CommonModule,
@@ -203,6 +204,7 @@ const PROVIDERS = [
TaskResponseParsingService,
ClaimedTaskDataService,
PoolTaskDataService,
BitstreamDataService,
// register AuthInterceptor as HttpInterceptor
{
provide: HTTP_INTERCEPTORS,

View File

@@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { DataService } from './data.service';
import { Bitstream } from '../shared/bitstream.model';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { BrowseService } from '../browse/browse.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { FindAllOptions } from './request.models';
import { Observable } from 'rxjs/internal/Observable';
/**
* A service responsible for fetching/sending data from/to the REST API on the bitstreams endpoint
*/
@Injectable()
export class BitstreamDataService extends DataService<Bitstream> {
protected linkPath = 'bitstreams';
protected forceBypassCache = false;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected dataBuildService: NormalizedObjectBuildService,
protected store: Store<CoreState>,
protected bs: BrowseService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DSOChangeAnalyzer<Bitstream>) {
super();
}
/**
* Get the endpoint for browsing bitstreams
* @param {FindAllOptions} options
* @param linkPath
* @returns {Observable<string>}
*/
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return this.halService.getEndpoint(linkPath);
}
}

View File

@@ -118,4 +118,14 @@ export class ItemDataService extends DataService<Item> {
map((requestEntry: RequestEntry) => requestEntry.response)
);
}
/**
* Get the endpoint for an item's bitstreams
* @param itemId
*/
public getBitstreamsEndpoint(itemId: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
map((url: string) => `${url}/${itemId}/bitstreams`)
);
}
}

View File

@@ -0,0 +1,21 @@
import { MetadataMap } from '../../core/shared/metadata.models';
/**
* Properties to send to the REST API for uploading a bitstream
*/
export class UploaderProperties {
/**
* A custom name for the bitstream
*/
name: string;
/**
* Metadata for the bitstream (e.g. dc.description)
*/
metadata: MetadataMap;
/**
* The name of the bundle to upload the bitstream to
*/
bundleName: string;
}

View File

@@ -15,8 +15,9 @@ import { uniqueId } from 'lodash';
import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
import { UploaderOptions } from './uploader-options.model';
import { isNotEmpty, isUndefined } from '../empty.util';
import { hasValue, isNotEmpty, isUndefined } from '../empty.util';
import { UploaderService } from './uploader.service';
import { UploaderProperties } from './uploader-properties.model';
@Component({
selector: 'ds-uploader',
@@ -53,6 +54,11 @@ export class UploaderComponent {
*/
@Input() uploadFilesOptions: UploaderOptions;
/**
* Extra properties to be passed with the form-data of the upload
*/
@Input() uploadProperties: UploaderProperties;
/**
* The function to call when upload is completed
*/
@@ -127,6 +133,11 @@ export class UploaderComponent {
};
this.scrollToService.scrollTo(config);
};
if (hasValue(this.uploadProperties)) {
this.uploader.onBuildItemForm = (item, form) => {
form.append('properties', JSON.stringify(this.uploadProperties))
};
}
this.uploader.onCompleteItem = (item: any, response: any, status: any, headers: any) => {
if (isNotEmpty(response)) {
const responsePath = JSON.parse(response);