fixed file upload, styling, boolean/date/output inputs

This commit is contained in:
lotte
2020-03-24 18:05:22 +01:00
committed by Art Lowel
parent 3e4704af0d
commit e38aec831f
34 changed files with 291 additions and 69 deletions

View File

@@ -1614,6 +1614,24 @@
"process.new.select-parameters": "Parameters",
"process.new.submit": "Submit",
"process.new.select-script": "Script",
"process.new.select-script.placeholder": "Choose a script...",
"process.new.select-script.required": "Script is required",
"process.new.parameter.file.upload-button": "Select file...",
"process.new.parameter.file.required": "Please select a file",
"process.new.parameter.string.required": "Parameter value is required",
"publication.listelement.badge": "Publication",
"publication.page.description": "Description",

View File

@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import { DataService } from '../data.service';
import { RequestService } from '../request.service';
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../../core.reducers';
@@ -10,6 +9,11 @@ import { NotificationsService } from '../../../shared/notifications/notification
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
import { Script } from '../../../process-page/scripts/script.model';
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
import { map } from 'rxjs/operators';
import { URLCombiner } from '../../url-combiner/url-combiner';
import { MultipartPostRequest, RestRequest } from '../request.models';
import { RequestService } from '../request.service';
@Injectable()
export class ScriptDataService extends DataService<Script> {
@@ -26,4 +30,24 @@ export class ScriptDataService extends DataService<Script> {
protected comparator: DefaultChangeAnalyzer<Script>) {
super();
}
public invocate(scriptName: string, parameters: ProcessParameter[], files: File[]) {
this.getBrowseEndpoint().pipe(
map((endpoint: string) => new URLCombiner(endpoint, scriptName, 'processes').toString()),
map((endpoint: string) => {
const body = this.getInvocationFormData(parameters, files);
return new MultipartPostRequest(this.requestService.generateRequestId(), endpoint, body)
}),
map((request: RestRequest) => this.requestService.configure(request))
).subscribe();
}
private getInvocationFormData(parameters: ProcessParameter[], files: File[]): FormData {
const form: FormData = new FormData();
form.set('properties', JSON.stringify(parameters));
files.forEach((file: File) => {
form.append('file', file);
});
return form;
}
}

View File

@@ -12,12 +12,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
import {
RequestActionTypes,
RequestCompleteAction,
RequestExecuteAction,
ResetResponseTimestampsAction
} from './request.actions';
import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction, ResetResponseTimestampsAction } from './request.actions';
import { RequestError, RestRequest } from './request.models';
import { RequestEntry } from './request.reducer';
import { RequestService } from './request.service';
@@ -43,12 +38,12 @@ export class RequestEffects {
filter((entry: RequestEntry) => hasValue(entry)),
map((entry: RequestEntry) => entry.request),
flatMap((request: RestRequest) => {
let body;
if (isNotEmpty(request.body)) {
let body = request.body;
if (isNotEmpty(request.body) && !request.isMultipart) {
const serializer = new DSpaceSerializer(getClassForType(request.body.type));
body = serializer.serialize(request.body);
}
return this.restApi.request(request.method, request.href, body, request.options).pipe(
return this.restApi.request(request.method, request.href, body, request.options, request.isMultipart).pipe(
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
addToResponseCacheAndCompleteAction(request, this.EnvConfig),
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(

View File

@@ -32,6 +32,8 @@ export enum IdentifierType {
export abstract class RestRequest {
public responseMsToLive = 10 * 1000;
public forceBypassCache = false;
public isMultipart = false;
constructor(
public uuid: string,
public href: string,
@@ -74,6 +76,18 @@ export class PostRequest extends RestRequest {
}
}
export class MultipartPostRequest extends RestRequest {
public isMultipart = true;
constructor(
public uuid: string,
public href: string,
public body?: any,
public options?: HttpOptions
) {
super(uuid, href, RestRequestMethod.POST, body)
}
}
export class PutRequest extends RestRequest {
constructor(
public uuid: string,

View File

@@ -72,7 +72,7 @@ export class DSpaceRESTv2Service {
* @return {Observable<string>}
* An Observable<string> containing the response from the server
*/
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable<DSpaceRESTV2Response> {
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions, isMultipart?: boolean): Observable<DSpaceRESTV2Response> {
const requestOptions: HttpOptions = {};
requestOptions.body = body;
if (method === RestRequestMethod.POST && isNotEmpty(body) && isNotEmpty(body.name)) {
@@ -98,7 +98,7 @@ export class DSpaceRESTv2Service {
requestOptions.withCredentials = options.withCredentials;
}
if (!requestOptions.headers.has('Content-Type')) {
if (!requestOptions.headers.has('Content-Type') && !isMultipart) {
// Because HttpHeaders is immutable, the set method returns a new object instead of updating the existing headers
requestOptions.headers = requestOptions.headers.set('Content-Type', DEFAULT_CONTENT_TYPE);
}

View File

@@ -1,14 +1,16 @@
<script src="process-parameters/parameter-value-input/value-input.component.ts"></script>
<div class="container">
<div class="row">
<div class="col-12 col-md-6">
<form>
<ds-scripts-select (select)="selectScript($event)"></ds-scripts-select>
<ds-process-parameters [script]="selectedScript"></ds-process-parameters>
<form #form="ngForm" (ngSubmit)="submitForm()">
<ds-scripts-select (select)="selectedScript = $event"></ds-scripts-select>
<ds-process-parameters [script]="selectedScript" (updateParameters)="parameters = $event"></ds-process-parameters>
<button [disabled]="form.invalid" type="submit" class="btn btn-light float-right">{{ 'process.new.submit' | translate }}</button>
</form>
</div>
<div class="col-12 col-md-6">
<ds-script-help [script]="selectedScript"></ds-script-help>
</div>
</div>
</div>

View File

@@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { Script } from '../scripts/script.model';
import { Process } from '../processes/process.model';
import { ProcessParameter } from '../processes/process-parameter.model';
import { ScriptDataService } from '../../core/data/processes/script-data.service';
@Component({
selector: 'ds-new-process',
@@ -10,13 +12,32 @@ import { Process } from '../processes/process.model';
export class NewProcessComponent implements OnInit {
public selectedScript: Script;
public process: Process;
public parameters: ProcessParameter[];
public files: File[] = [];
constructor(private scriptService: ScriptDataService) {
}
ngOnInit(): void {
this.process = new Process();
}
selectScript(script: Script) {
this.selectedScript = script;
console.log('selected script: ', script);
submitForm() {
const stringParameters: ProcessParameter[] = this.parameters.map((parameter: ProcessParameter) => {
return {
name: parameter.name,
value: this.checkValue(parameter)
};
}
);
this.scriptService.invocate(this.selectedScript.id, stringParameters, this.files)
}
checkValue(processParameter: ProcessParameter): string {
if (typeof processParameter.value === 'object') {
this.files = [...this.files, processParameter.value];
return processParameter.value.name;
}
return processParameter.value;
}
}

View File

@@ -9,7 +9,7 @@
{{param.nameLong || param.name}}
</option>
</select>
<ds-parameter-value-input [parameter]="selectedScriptParameter" class="d-block col"></ds-parameter-value-input>
<ds-parameter-value-input [parameter]="selectedScriptParameter" (updateValue)="selectedParameterValue = $event" class="d-block col"></ds-parameter-value-input>
<button *ngIf="removable" class="btn btn-light col-1" (click)="removeParameter.emit(parameterValue);"><span class="fas fa-trash"></span></button>
<span *ngIf="!removable" class="col-1"></span>
</div>

View File

@@ -31,6 +31,7 @@ export class ParameterSelectComponent implements OnInit {
set selectedParameter(value: string) {
this.parameterValue.name = value;
this.selectedParameterValue = undefined;
this.changeParameter.emit(this.parameterValue);
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BooleanValueInputComponent } from './boolean-value-input.component';
describe('StringValueInputComponent', () => {
let component: BooleanValueInputComponent;
let fixture: ComponentFixture<BooleanValueInputComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BooleanValueInputComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BooleanValueInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,13 @@
import { Component, OnInit } from '@angular/core';
import { ValueInputComponent } from '../value-input.component';
@Component({
selector: 'ds-boolean-value-input',
templateUrl: './boolean-value-input.component.html',
styleUrls: ['./boolean-value-input.component.scss']
})
export class BooleanValueInputComponent extends ValueInputComponent<boolean> implements OnInit {
ngOnInit() {
this.updateValue.emit(true)
}
}

View File

@@ -0,0 +1,7 @@
<input required #string="ngModel" type="date" class="form-control" id="string-value-input" [ngModel]="value" (ngModelChange)="setValue($event)"/>
<div *ngIf="string.invalid && (string.dirty || string.touched)"
class="alert alert-danger">
<div *ngIf="string.errors.required">
{{'process.new.parameter.string.required' | translate}}
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DateValueInputComponent } from './date-value-input.component';
describe('StringValueInputComponent', () => {
let component: DateValueInputComponent;
let fixture: ComponentFixture<DateValueInputComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DateValueInputComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DateValueInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { ValueInputComponent } from '../value-input.component';
@Component({
selector: 'ds-date-value-input',
templateUrl: './date-value-input.component.html',
styleUrls: ['./date-value-input.component.scss']
})
export class DateValueInputComponent extends ValueInputComponent<string> {
value: string;
setValue(value) {
this.value = value;
this.updateValue.emit(value)
}
}

View File

@@ -1,6 +1,10 @@
<ds-uploader dropMsg="process.parameter.file-upload"
dropOverDocumentMsg="process.parameter.file-upload"
[enableDragOverDocument]="true"
[uploadFilesOptions]="uploadFilesOptions"
(onCompleteItem)="onCompleteItem()"
(onUploadError)="onUploadError()"></ds-uploader>
<label for="file-upload" class="btn btn-light">
{{'process.new.parameter.file.upload-button' | translate}}
</label>
<input requireFile #file="ngModel" type="file" id="file-upload" class="form-control-file d-none" [ngModel]="file" (ngModelChange)="setFile($event)"/>
<div *ngIf="file.invalid && (file.dirty || file.touched)"
class="alert alert-danger">
<div *ngIf="file.errors.required">
{{'process.new.parameter.file.required' | translate}}
</div>
</div>

View File

@@ -1,33 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { FileUploaderOptions } from 'ng2-file-upload';
import { UploaderOptions } from '../../../../../shared/uploader/uploader-options.model';
import { Component } from '@angular/core';
import { ValueInputComponent } from '../value-input.component';
@Component({
selector: 'ds-file-value-input',
templateUrl: './file-value-input.component.html',
styleUrls: ['./file-value-input.component.scss']
})
export class FileValueInputComponent implements OnInit {
uploadFilesOptions: FileUploaderOptions;
constructor() {
}
ngOnInit() {
this.uploadFilesOptions = new UploaderOptions();
this.uploadFilesOptions.autoUpload = false;
this.uploadFilesOptions.url = 'bladibla';
this.uploadFilesOptions.authToken = 'bladibla';
this.uploadFilesOptions.disableMultipart = true;
this.uploadFilesOptions.formatDataFunctionIsAsync = false;
this.uploadFilesOptions.formatDataFunction((t) => console.log(t));
}
onCompleteItem() {
}
onUploadError() {
export class FileValueInputComponent extends ValueInputComponent<File> {
file: File;
setFile(files) {
this.file = files.length > 0 ? files[0] : undefined;
console.log(this.file);
this.updateValue.emit(this.file);
}
}

View File

@@ -1,4 +1,7 @@
<div [ngSwitch]="parameter?.type">
<ds-string-value-input *ngSwitchCase="parameterTypes.STRING"></ds-string-value-input>
<ds-file-value-input *ngSwitchCase="parameterTypes.FILE"></ds-file-value-input>
<ds-string-value-input *ngSwitchCase="parameterTypes.STRING" (updateValue)="updateValue.emit($event)"></ds-string-value-input>
<ds-string-value-input *ngSwitchCase="parameterTypes.OUTPUT" (updateValue)="updateValue.emit($event)"></ds-string-value-input>
<ds-date-value-input *ngSwitchCase="parameterTypes.DATE" (updateValue)="updateValue.emit($event)"></ds-date-value-input>
<ds-file-value-input *ngSwitchCase="parameterTypes.FILE" (updateValue)="updateValue.emit($event)"></ds-file-value-input>
<ds-boolean-value-input *ngSwitchCase="parameterTypes.BOOLEAN" (updateValue)="updateValue.emit($event)"></ds-boolean-value-input>
</div>

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { ScriptParameterType } from '../../../scripts/script-parameter-type.model';
import { ScriptParameter } from '../../../scripts/script-parameter.model';
@@ -9,5 +9,6 @@ import { ScriptParameter } from '../../../scripts/script-parameter.model';
})
export class ParameterValueInputComponent {
@Input() parameter: ScriptParameter;
@Output() updateValue: EventEmitter<any> = new EventEmitter();
parameterTypes = ScriptParameterType;
}

View File

@@ -1 +1,7 @@
<input type="input" class="form-control" id="string-value-input">
<input required #string="ngModel" type="text" class="form-control" id="string-value-input" [ngModel]="value" (ngModelChange)="setValue($event)"/>
<div *ngIf="string.invalid && (string.dirty || string.touched)"
class="alert alert-danger">
<div *ngIf="string.errors.required">
{{'process.new.parameter.string.required' | translate}}
</div>
</div>

View File

@@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { ValueInputComponent } from '../value-input.component';
@Component({
selector: 'ds-string-value-input',
templateUrl: './string-value-input.component.html',
styleUrls: ['./string-value-input.component.scss']
})
export class StringValueInputComponent implements OnInit {
export class StringValueInputComponent extends ValueInputComponent<string> {
value: string;
constructor() { }
ngOnInit() {
setValue(value) {
this.value = value;
this.updateValue.emit(value)
}
}

View File

@@ -0,0 +1,5 @@
import { EventEmitter, Output } from '@angular/core';
export abstract class ValueInputComponent<T> {
@Output() updateValue: EventEmitter<T> = new EventEmitter<T>()
}

View File

@@ -1,5 +1,5 @@
<div class="form-group" *ngIf="script">
<label>Parameters</label>
<label>{{'process.new.select-parameters' | translate}}</label>
<ds-parameter-select
*ngFor="let value of parameterValues; let i = index; let last = last"
[parameters]="script.parameters"

View File

@@ -1,6 +1,7 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { Script } from '../../scripts/script.model';
import { ProcessParameter } from '../../processes/process-parameter.model';
import { hasValue } from '../../../shared/empty.util';
@Component({
selector: 'ds-process-parameters',
@@ -9,6 +10,7 @@ import { ProcessParameter } from '../../processes/process-parameter.model';
})
export class ProcessParametersComponent implements OnChanges {
@Input() script: Script;
@Output() updateParameters: EventEmitter<ProcessParameter[]> = new EventEmitter();
parameterValues: ProcessParameter[];
ngOnChanges(changes: SimpleChanges): void {
@@ -27,6 +29,7 @@ export class ProcessParametersComponent implements OnChanges {
if (index === this.parameterValues.length - 1) {
this.addParameter();
}
this.updateParameters.emit(this.parameterValues.filter((param: ProcessParameter) => hasValue(param.name)));
}
removeParameter(index: number) {

View File

@@ -1,11 +1,11 @@
<div class="form-group" *ngIf="scripts$ | async">
<label for="process-script">Script</label>
<label for="process-script"><label>{{'process.new.select-script' | translate}}</label></label>
<select required id="process-script"
class="form-control"
name="script"
[(ngModel)]="selectedScript"
#script="ngModel">
<option [ngValue]="undefined">Choose a script...</option>
<option [ngValue]="undefined">{{'process.new.select-script.placeholder' | translate}}</option>
<option *ngFor="let script of scripts$ | async" [ngValue]="script.id">
{{script.name}}
</option>
@@ -14,7 +14,7 @@
<div *ngIf="script.invalid && (script.dirty || script.touched)"
class="alert alert-danger">
<div *ngIf="script.errors.required">
Script is required.
{{'process.new.select-script.required' | translate}}
</div>
</div>
</div>

View File

@@ -9,6 +9,8 @@ import { ProcessParametersComponent } from './new/process-parameters/process-par
import { StringValueInputComponent } from './new/process-parameters/parameter-value-input/string-value-input/string-value-input.component';
import { ParameterValueInputComponent } from './new/process-parameters/parameter-value-input/parameter-value-input.component';
import { FileValueInputComponent } from './new/process-parameters/parameter-value-input/file-value-input/file-value-input.component';
import { BooleanValueInputComponent } from './new/process-parameters/parameter-value-input/boolean-value-input/boolean-value-input.component';
import { DateValueInputComponent } from './new/process-parameters/parameter-value-input/date-value-input/date-value-input.component';
@NgModule({
imports: [
@@ -24,6 +26,8 @@ import { FileValueInputComponent } from './new/process-parameters/parameter-valu
StringValueInputComponent,
ParameterValueInputComponent,
FileValueInputComponent,
BooleanValueInputComponent,
DateValueInputComponent,
],
entryComponents: []
})

View File

@@ -6,8 +6,9 @@ export class ProcessParameter {
* The name of the parameter Eg. '-d', '-f' etc.
*/
name: string;
/**
* The value of the parameter
*/
value: string;
value: any;
}

View File

@@ -63,10 +63,10 @@ export class EndpointMockingRestService extends DSpaceRESTv2Service {
* @return Observable<DSpaceRESTV2Response>
* An Observable<DSpaceRESTV2Response> containing the response from the server
*/
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable<DSpaceRESTV2Response> {
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions, isMultipart?: boolean): Observable<DSpaceRESTV2Response> {
const mockData = this.getMockData(url);
if (isEmpty(mockData)) {
return super.request(method, url, body, options);
return super.request(method, url, body, options, isMultipart);
} else {
return this.toMockResponse$(mockData);
}

View File

@@ -186,6 +186,8 @@ import { LogInPasswordComponent } from './log-in/methods/password/log-in-passwor
import { LogInComponent } from './log-in/log-in.component';
import { MissingTranslationHelper } from './translate/missing-translation.helper';
import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component';
import { FileValidator } from './utils/require-file.validator';
import { FileValueAccessorDirective } from './utils/file-value-accessor.directive';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -452,7 +454,9 @@ const DIRECTIVES = [
AutoFocusDirective,
RoleDirective,
MetadataRepresentationDirective,
ListableObjectDirective
ListableObjectDirective,
FileValueAccessorDirective,
FileValidator
];
@NgModule({

View File

@@ -146,6 +146,8 @@ export class UploaderComponent {
};
this.uploader.onProgressAll = () => this.onProgress();
this.uploader.onProgressItem = () => this.onProgress();
console.log(this.uploader.options.formatDataFunction());
}
/**

View File

@@ -0,0 +1,24 @@
import {Directive} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
@Directive({
// tslint:disable-next-line:directive-selector
selector: 'input[type=file]',
// tslint:disable-next-line:no-host-metadata-property
host : {
'(change)' : 'onChange($event.target.files)',
'(blur)': 'onTouched()'
},
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: FileValueAccessorDirective, multi: true }
]
})
export class FileValueAccessorDirective implements ControlValueAccessor {
value: any;
onChange = (_) => { /* empty */ };
onTouched = () => { /* empty */};
writeValue(value) { /* empty */}
registerOnChange(fn: any) { this.onChange = fn; }
registerOnTouched(fn: any) { this.onTouched = fn; }
}

View File

@@ -0,0 +1,19 @@
import {Directive} from '@angular/core';
import {NG_VALIDATORS, Validator, FormControl} from '@angular/forms';
@Directive({
// tslint:disable-next-line:directive-selector
selector: '[requireFile]',
providers: [
{ provide: NG_VALIDATORS, useExisting: FileValidator, multi: true },
]
})
export class FileValidator implements Validator {
static validate(c: FormControl): {[key: string]: any} {
return c.value == null || c.value.length === 0 ? { required : true} : null;
}
validate(c: FormControl): {[key: string]: any} {
return FileValidator.validate(c);
}
}