added create-from functionality

This commit is contained in:
lotte
2020-06-29 22:17:41 +02:00
parent 32c8d9de03
commit 1cb0bb72e4
50 changed files with 408 additions and 237 deletions

View File

@@ -1,38 +1,42 @@
<div class="container" *ngVar="(processRD$ | async)?.payload as process"> <div class="container" *ngVar="(processRD$ | async)?.payload as process">
<h2>{{'process.detail.title' | translate:{ id: process?.processId, name: process?.scriptName } }}</h2> <div class="d-flex">
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2>
<ds-process-detail-field id="process-name" [title]="'process.detail.script'"> <div>
<div>{{ process?.scriptName }}</div> <a class="btn btn-light" [routerLink]="'/processes/new'" [queryParams]="{id: process?.processId}">{{'process.detail.create' | translate}}</a>
</ds-process-detail-field> </div>
</div>
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments" [title]="'process.detail.arguments'"> <ds-process-detail-field id="process-name" [title]="'process.detail.script'">
<div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div> <div>{{ process?.scriptName }}</div>
</ds-process-detail-field>
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files" [title]="'process.detail.output-files'">
<div *ngFor="let file of files">
<span><a [href]="file?._links?.content?.href">{{getFileName(file)}}</a></span>
<span>({{file?.sizeBytes | dsFileSize}})</span>
</div>
</ds-process-detail-field> </ds-process-detail-field>
</div>
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate"> <ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments" [title]="'process.detail.arguments'">
<div>{{ process.startTime }}</div> <div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div>
</ds-process-detail-field> </ds-process-detail-field>
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time" [title]="'process.detail.end-time' | translate"> <div *ngVar="(filesRD$ | async)?.payload?.page as files">
<div>{{ process.endTime }}</div> <ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files" [title]="'process.detail.output-files'">
</ds-process-detail-field> <div *ngFor="let file of files">
<span><a [href]="file?._links?.content?.href">{{getFileName(file)}}</a></span>
<span>({{file?.sizeBytes | dsFileSize}})</span>
</div>
</ds-process-detail-field>
</div>
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate"> <ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate">
<div>{{ process.processStatus }}</div> <div>{{ process.startTime }}</div>
</ds-process-detail-field> </ds-process-detail-field>
<!--<ds-process-detail-field id="process-output" [title]="'process.detail.output'">--> <ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time" [title]="'process.detail.end-time' | translate">
<div>{{ process.endTime }}</div>
</ds-process-detail-field>
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate">
<div>{{ process.processStatus }}</div>
</ds-process-detail-field>
<!--<ds-process-detail-field id="process-output" [title]="'process.detail.output'">-->
<!--<pre class="font-weight-bold text-secondary bg-light p-3">{{'process.detail.output.alert' | translate}}</pre>--> <!--<pre class="font-weight-bold text-secondary bg-light p-3">{{'process.detail.output.alert' | translate}}</pre>-->
<!--</ds-process-detail-field>--> <!--</ds-process-detail-field>-->
<a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a> <a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
</div> </div>

View File

@@ -0,0 +1,25 @@
<div class="container">
<div class="row">
<h2 class="col-12">
{{headerKey | translate}}
</h2>
<div class="col-12 col-md-6">
<form #form="ngForm" (ngSubmit)="submitForm(form)">
<ds-scripts-select [script]="selectedScript" (select)="selectedScript = $event; parameters = undefined"></ds-scripts-select>
<ds-process-parameters [initialParams]="parameters" [script]="selectedScript" (updateParameters)="parameters = $event"></ds-process-parameters>
<button [routerLink]="['/processes']" class="btn btn-light float-left">{{ 'process.new.cancel' | translate }}</button>
<button 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 *ngIf="missingParameters.length > 0" class="alert alert-danger validation-error">
{{'process.new.parameter.required.missing' | translate}}
<ul>
<li *ngFor="let missing of missingParameters">{{missing}}</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,86 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ProcessFormComponent } from './process-form.component';
import { ScriptDataService } from '../../core/data/processes/script-data.service';
import { ScriptParameter } from '../scripts/script-parameter.model';
import { Script } from '../scripts/script.model';
import { ProcessParameter } from '../processes/process-parameter.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { of as observableOf } from 'rxjs';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { RequestService } from '../../core/data/request.service';
import { Router } from '@angular/router';
describe('NewProcessComponent', () => {
let component: ProcessFormComponent;
let fixture: ComponentFixture<ProcessFormComponent>;
let scriptService;
let parameterValues;
let script;
function init() {
const param1 = new ScriptParameter();
const param2 = new ScriptParameter();
script = Object.assign(new Script(), { parameters: [param1, param2] });
parameterValues = [
Object.assign(new ProcessParameter(), { name: '-a', value: 'bla' }),
Object.assign(new ProcessParameter(), { name: '-b', value: '123' }),
Object.assign(new ProcessParameter(), { name: '-c', value: 'value' }),
];
scriptService = jasmine.createSpyObj(
'scriptService',
{
invoke: observableOf({
response:
{
isSuccessful: true
}
})
}
)
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
imports: [
FormsModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
})],
declarations: [ProcessFormComponent],
providers: [
{ provide: ScriptDataService, useValue: scriptService },
{ provide: NotificationsService, useClass: NotificationsServiceStub },
{ provide: RequestService, useValue: {} },
{ provide: Router, useValue: {} },
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProcessFormComponent);
component = fixture.componentInstance;
component.parameters = parameterValues;
component.selectedScript = script;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call invoke on the scriptService on submit', () => {
component.submitForm({ invalid: false } as any);
expect(scriptService.invoke).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,146 @@
import { Component, Input, 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';
import { ControlContainer, NgForm } from '@angular/forms';
import { ScriptParameter } from '../scripts/script-parameter.model';
import { RequestEntry } from '../../core/data/request.reducer';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs/operators';
import { RequestService } from '../../core/data/request.service';
import { Router } from '@angular/router';
/**
* Component to create a new script
*/
@Component({
selector: 'ds-process-form',
templateUrl: './process-form.component.html',
styleUrls: ['./process-form.component.scss'],
})
export class ProcessFormComponent implements OnInit {
/**
* The currently selected script
*/
@Input() public selectedScript: Script = undefined;
/**
* The process to create
*/
@Input() public process: Process = undefined;
/**
* The parameter values to use to start the process
*/
@Input() public parameters: ProcessParameter[];
/**
* Optional files that are used as parameter values
*/
public files: File[] = [];
@Input() public headerKey: string;
/**
* Contains the missing parameters on submission
*/
public missingParameters = [];
constructor(
private scriptService: ScriptDataService,
private notificationsService: NotificationsService,
private translationService: TranslateService,
private requestService: RequestService,
private router: Router) {
}
ngOnInit(): void {
this.process = new Process();
}
/**
* Validates the form, sets the parameters to correct values and invokes the script with the correct parameters
* @param form
*/
submitForm(form: NgForm) {
if (!this.validateForm(form) || this.isRequiredMissing()) {
return;
}
const stringParameters: ProcessParameter[] = this.parameters.map((parameter: ProcessParameter) => {
return {
name: parameter.name,
value: this.checkValue(parameter)
};
}
);
this.scriptService.invoke(this.selectedScript.id, stringParameters, this.files)
.pipe(take(1))
.subscribe((requestEntry: RequestEntry) => {
if (requestEntry.response.isSuccessful) {
const title = this.translationService.get('process.new.notification.success.title');
const content = this.translationService.get('process.new.notification.success.content');
this.notificationsService.success(title, content)
this.sendBack();
} else {
const title = this.translationService.get('process.new.notification.error.title');
const content = this.translationService.get('process.new.notification.error.content');
this.notificationsService.error(title, content)
}
})
}
/**
* Checks whether the parameter values are files
* Replaces file parameters by strings and stores the files in a separate list
* @param processParameter The parameter value to check
*/
private checkValue(processParameter: ProcessParameter): string {
if (typeof processParameter.value === 'object') {
this.files = [...this.files, processParameter.value];
return processParameter.value.name;
}
return processParameter.value;
}
/**
* Validates the form
* Returns false if the form is invalid
* Returns true if the form is valid
* @param form The NgForm object to validate
*/
private validateForm(form: NgForm) {
if (form.invalid) {
Object.keys(form.controls).forEach((key) => {
form.controls[key].markAsDirty();
});
return false;
}
return true;
}
private isRequiredMissing() {
const setParams: string[] = this.parameters
.map((param) => param.name);
const requiredParams: ScriptParameter[] = this.selectedScript.parameters.filter((param) => param.mandatory);
for (const rp of requiredParams) {
if (!setParams.includes(rp.name)) {
this.missingParameters.push(rp.name);
}
}
return this.missingParameters.length > 0;
}
private sendBack() {
this.requestService.removeByHrefSubstring('/processes');
/* should subscribe on the previous method to know the action is finished and then navigate,
will fix this when the removeByHrefSubstring changes are merged */
this.router.navigateByUrl('/processes');
}
}
export function controlContainerFactory(controlContainer?: ControlContainer) {
return controlContainer;
}

View File

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

View File

@@ -3,7 +3,7 @@ import { ProcessParameter } from '../../../processes/process-parameter.model';
import { ScriptParameter } from '../../../scripts/script-parameter.model'; import { ScriptParameter } from '../../../scripts/script-parameter.model';
import { hasNoValue } from '../../../../shared/empty.util'; import { hasNoValue } from '../../../../shared/empty.util';
import { ControlContainer, NgForm } from '@angular/forms'; import { ControlContainer, NgForm } from '@angular/forms';
import { controlContainerFactory } from '../../new-process.component'; import { controlContainerFactory } from '../../process-form.component';
/** /**
* Component to select a single parameter for a process * Component to select a single parameter for a process
@@ -12,17 +12,19 @@ import { controlContainerFactory } from '../../new-process.component';
selector: 'ds-parameter-select', selector: 'ds-parameter-select',
templateUrl: './parameter-select.component.html', templateUrl: './parameter-select.component.html',
styleUrls: ['./parameter-select.component.scss'], styleUrls: ['./parameter-select.component.scss'],
viewProviders: [ { provide: ControlContainer, viewProviders: [{
provide: ControlContainer,
useFactory: controlContainerFactory, useFactory: controlContainerFactory,
deps: [[new Optional(), NgForm]] } ] deps: [[new Optional(), NgForm]]
}]
}) })
export class ParameterSelectComponent implements OnInit { export class ParameterSelectComponent {
@Input() index: number; @Input() index: number;
/** /**
* The current parameter value of the selected parameter * The current parameter value of the selected parameter
*/ */
@Input() parameterValue: ProcessParameter; @Input() parameterValue: ProcessParameter = new ProcessParameter();
/** /**
* The available script parameters for the script * The available script parameters for the script
@@ -44,12 +46,6 @@ export class ParameterSelectComponent implements OnInit {
*/ */
@Output() changeParameter: EventEmitter<ProcessParameter> = new EventEmitter<ProcessParameter>(); @Output() changeParameter: EventEmitter<ProcessParameter> = new EventEmitter<ProcessParameter>();
ngOnInit(): void {
if (hasNoValue(this.parameterValue)) {
this.parameterValue = new ProcessParameter();
}
}
/** /**
* Returns the script parameter based on the currently selected name * Returns the script parameter based on the currently selected name
*/ */

View File

@@ -1,7 +1,7 @@
import { Component, OnInit, Optional } from '@angular/core'; import { Component, OnInit, Optional } from '@angular/core';
import { ValueInputComponent } from '../value-input.component'; import { ValueInputComponent } from '../value-input.component';
import { ControlContainer, NgForm } from '@angular/forms'; import { ControlContainer, NgForm } from '@angular/forms';
import { controlContainerFactory } from '../../../new-process.component'; import { controlContainerFactory } from '../../../process-form.component';
/** /**
* Represents the value of a boolean parameter * Represents the value of a boolean parameter

View File

@@ -1,7 +1,7 @@
import { Component, OnInit, Optional } from '@angular/core'; import { Component, OnInit, Optional, Input } from '@angular/core';
import { ValueInputComponent } from '../value-input.component'; import { ValueInputComponent } from '../value-input.component';
import { ControlContainer, NgForm } from '@angular/forms'; import { ControlContainer, NgForm } from '@angular/forms';
import { controlContainerFactory } from '../../../new-process.component'; import { controlContainerFactory } from '../../../process-form.component';
/** /**
* Represents the user inputted value of a date parameter * Represents the user inputted value of a date parameter
@@ -19,6 +19,11 @@ export class DateValueInputComponent extends ValueInputComponent<string> {
* The current value of the date string * The current value of the date string
*/ */
value: string; value: string;
@Input() initialValue;
ngOnInit() {
this.value = this.initialValue;
}
setValue(value) { setValue(value) {
this.value = value; this.value = value;

View File

@@ -1,7 +1,7 @@
import { Component, Optional } from '@angular/core'; import { Component, Optional } from '@angular/core';
import { ValueInputComponent } from '../value-input.component'; import { ValueInputComponent } from '../value-input.component';
import { ControlContainer, NgForm } from '@angular/forms'; import { ControlContainer, NgForm } from '@angular/forms';
import { controlContainerFactory } from '../../../new-process.component'; import { controlContainerFactory } from '../../../process-form.component';
/** /**
* Represents the user inputted value of a file parameter * Represents the user inputted value of a file parameter

View File

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

View File

@@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Optional, Output } f
import { ScriptParameterType } from '../../../scripts/script-parameter-type.model'; import { ScriptParameterType } from '../../../scripts/script-parameter-type.model';
import { ScriptParameter } from '../../../scripts/script-parameter.model'; import { ScriptParameter } from '../../../scripts/script-parameter.model';
import { ControlContainer, NgForm } from '@angular/forms'; import { ControlContainer, NgForm } from '@angular/forms';
import { controlContainerFactory } from '../../new-process.component'; import { controlContainerFactory } from '../../process-form.component';
/** /**
* Component that renders the correct parameter value input based the script parameter's type * Component that renders the correct parameter value input based the script parameter's type
@@ -23,6 +23,8 @@ export class ParameterValueInputComponent {
*/ */
@Input() parameter: ScriptParameter; @Input() parameter: ScriptParameter;
@Input() initialValue: any;
/** /**
* Emits the value of the input when its updated * Emits the value of the input when its updated
*/ */

View File

@@ -1,7 +1,7 @@
import { Component, Optional } from '@angular/core'; import { Component, Optional, Input } from '@angular/core';
import { ValueInputComponent } from '../value-input.component'; import { ValueInputComponent } from '../value-input.component';
import { ControlContainer, NgForm } from '@angular/forms'; import { ControlContainer, NgForm } from '@angular/forms';
import { controlContainerFactory } from '../../../new-process.component'; import { controlContainerFactory } from '../../../process-form.component';
/** /**
* Represents the user inputted value of a string parameter * Represents the user inputted value of a string parameter
@@ -19,7 +19,12 @@ export class StringValueInputComponent extends ValueInputComponent<string> {
* The current value of the string * The current value of the string
*/ */
value: string; value: string;
@Input() initialValue;
ngOnInit() {
this.value = this.initialValue;
}
setValue(value) { setValue(value) {
this.value = value; this.value = value;
this.updateValue.emit(value) this.updateValue.emit(value)

View File

@@ -4,7 +4,7 @@ import { ProcessParameter } from '../../processes/process-parameter.model';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { ControlContainer, NgForm } from '@angular/forms'; import { ControlContainer, NgForm } from '@angular/forms';
import { ScriptParameter } from '../../scripts/script-parameter.model'; import { ScriptParameter } from '../../scripts/script-parameter.model';
import { controlContainerFactory } from '../new-process.component'; import { controlContainerFactory } from '../process-form.component';
/** /**
* Component that represents the selected list of parameters for a script * Component that represents the selected list of parameters for a script
@@ -13,15 +13,18 @@ import { controlContainerFactory } from '../new-process.component';
selector: 'ds-process-parameters', selector: 'ds-process-parameters',
templateUrl: './process-parameters.component.html', templateUrl: './process-parameters.component.html',
styleUrls: ['./process-parameters.component.scss'], styleUrls: ['./process-parameters.component.scss'],
viewProviders: [ { provide: ControlContainer, viewProviders: [{
provide: ControlContainer,
useFactory: controlContainerFactory, useFactory: controlContainerFactory,
deps: [[new Optional(), NgForm]] } ] deps: [[new Optional(), NgForm]]
}]
}) })
export class ProcessParametersComponent implements OnChanges { export class ProcessParametersComponent implements OnChanges {
/** /**
* The currently selected script * The currently selected script
*/ */
@Input() script: Script; @Input() script: Script;
@Input() initialParams: ProcessParameter[];
/** /**
* Emits the parameter values when they're updated * Emits the parameter values when they're updated
*/ */
@@ -32,6 +35,12 @@ export class ProcessParametersComponent implements OnChanges {
*/ */
parameterValues: ProcessParameter[]; parameterValues: ProcessParameter[];
ngOnInit() {
if (hasValue(this.initialParams)) {
this.parameterValues = this.initialParams;
}
}
/** /**
* Makes sure the parameters are reset when the script changes * Makes sure the parameters are reset when the script changes
* @param changes * @param changes
@@ -47,8 +56,12 @@ export class ProcessParametersComponent implements OnChanges {
* Initializes the first parameter value * Initializes the first parameter value
*/ */
initParameters() { initParameters() {
this.parameterValues = []; if (hasValue(this.initialParams)) {
this.initializeParameter(); this.parameterValues = this.initialParams;
} else {
this.parameterValues = [];
this.initializeParameter();
}
} }
/** /**

View File

@@ -1,14 +1,14 @@
import { Component, EventEmitter, OnDestroy, OnInit, Optional, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Optional, Output } from '@angular/core';
import { ScriptDataService } from '../../../core/data/processes/script-data.service'; import { ScriptDataService } from '../../../core/data/processes/script-data.service';
import { Script } from '../../scripts/script.model'; import { Script } from '../../scripts/script.model';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Params, Router } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { hasValue } from '../../../shared/empty.util'; import { hasNoValue, hasValue } from '../../../shared/empty.util';
import { ControlContainer, FormControl, NgForm } from '@angular/forms'; import { ControlContainer, NgForm } from '@angular/forms';
import { controlContainerFactory } from '../new-process.component'; import { controlContainerFactory } from '../process-form.component';
const SCRIPT_QUERY_PARAMETER = 'script'; const SCRIPT_QUERY_PARAMETER = 'script';
@@ -50,6 +50,7 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
this.routeSub = this.route.queryParams this.routeSub = this.route.queryParams
.pipe( .pipe(
filter((params: Params) => hasNoValue(params.id)),
map((params: Params) => params[SCRIPT_QUERY_PARAMETER]), map((params: Params) => params[SCRIPT_QUERY_PARAMETER]),
distinctUntilChanged(), distinctUntilChanged(),
switchMap((id: string) => switchMap((id: string) =>
@@ -82,11 +83,15 @@ export class ScriptsSelectComponent implements OnInit, OnDestroy {
this.router.navigate([], this.router.navigate([],
{ {
queryParams: { [SCRIPT_QUERY_PARAMETER]: value }, queryParams: { [SCRIPT_QUERY_PARAMETER]: value },
queryParamsHandling: 'merge'
} }
); );
} }
@Input()
set script(value: Script) {
this._selectedScript = value;
}
ngOnDestroy(): void { ngOnDestroy(): void {
if (hasValue(this.routeSub)) { if (hasValue(this.routeSub)) {
this.routeSub.unsubscribe(); this.routeSub.unsubscribe();

View File

@@ -1,25 +1,6 @@
<div class="container"> <ng-container *ngIf="fromExisting$ && (fromExisting$ | async)">
<div class="row"> <ds-process-form *ngVar="fromExisting$ | async as process" headerKey="process.new.header" [selectedScript]="script$ | async" [parameters]="process.parameters"></ds-process-form>
<h2 class="col-12"> </ng-container>
{{'process.new.header' | translate}} <ng-container *ngIf="!fromExisting$ || !(fromExisting$ | async)">
</h2> <ds-process-form headerKey="process.new.header"></ds-process-form>
<div class="col-12 col-md-6"> </ng-container>
<form #form="ngForm" (ngSubmit)="submitForm(form)">
<ds-scripts-select (select)="selectedScript = $event"></ds-scripts-select>
<ds-process-parameters [script]="selectedScript" (updateParameters)="parameters = $event"></ds-process-parameters>
<button [routerLink]="['/processes']" class="btn btn-light float-left">{{ 'process.new.cancel' | translate }}</button>
<button 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 *ngIf="missingParameters.length > 0" class="alert alert-danger validation-error">
{{'process.new.parameter.required.missing' | translate}}
<ul>
<li *ngFor="let missing of missingParameters">{{missing}}</li>
</ul>
</div>
</div>

View File

@@ -13,7 +13,10 @@ import { of as observableOf } from 'rxjs';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { RequestService } from '../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
import { Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { LinkService } from '../../core/cache/builders/link.service';
import { VarDirective } from '../../shared/utils/var.directive';
import { ProcessDataService } from '../../core/data/processes/process-data.service';
describe('NewProcessComponent', () => { describe('NewProcessComponent', () => {
let component: NewProcessComponent; let component: NewProcessComponent;
@@ -55,12 +58,14 @@ describe('NewProcessComponent', () => {
useClass: TranslateLoaderMock useClass: TranslateLoaderMock
} }
})], })],
declarations: [NewProcessComponent], declarations: [NewProcessComponent, VarDirective],
providers: [ providers: [
{ provide: ScriptDataService, useValue: scriptService }, { provide: ScriptDataService, useValue: scriptService },
{ provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: NotificationsService, useClass: NotificationsServiceStub },
{ provide: RequestService, useValue: {} }, { provide: RequestService, useValue: {} },
{ provide: Router, useValue: {} }, { provide: ActivatedRoute, useValue: { snapshot: { queryParams: {} } } },
{ provide: LinkService, useValue: {} },
{ provide: ProcessDataService, useValue: {} },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })
@@ -70,17 +75,10 @@ describe('NewProcessComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(NewProcessComponent); fixture = TestBed.createComponent(NewProcessComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.parameters = parameterValues;
component.selectedScript = script;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should call invoke on the scriptService on submit', () => {
component.submitForm({ invalid: false } as any);
expect(scriptService.invoke).toHaveBeenCalled();
});
}); });

View File

@@ -1,16 +1,13 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Script } from '../scripts/script.model';
import { Process } from '../processes/process.model'; import { Process } from '../processes/process.model';
import { ProcessParameter } from '../processes/process-parameter.model'; import { ActivatedRoute } from '@angular/router';
import { ScriptDataService } from '../../core/data/processes/script-data.service'; import { ProcessDataService } from '../../core/data/processes/process-data.service';
import { ControlContainer, NgForm } from '@angular/forms'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import { ScriptParameter } from '../scripts/script-parameter.model'; import { Observable } from 'rxjs';
import { RequestEntry } from '../../core/data/request.reducer'; import { map, switchMap } from 'rxjs/operators';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { LinkService } from '../../core/cache/builders/link.service';
import { TranslateService } from '@ngx-translate/core'; import { followLink } from '../../shared/utils/follow-link-config.model';
import { take } from 'rxjs/operators'; import { Script } from '../scripts/script.model';
import { RequestService } from '../../core/data/request.service';
import { Router } from '@angular/router';
/** /**
* Component to create a new script * Component to create a new script
@@ -21,124 +18,23 @@ import { Router } from '@angular/router';
styleUrls: ['./new-process.component.scss'], styleUrls: ['./new-process.component.scss'],
}) })
export class NewProcessComponent implements OnInit { export class NewProcessComponent implements OnInit {
/** fromExisting$?: Observable<Process>;
* The currently selected script script$?: Observable<Script>;
*/
public selectedScript: Script;
/** constructor(private route: ActivatedRoute, private processService: ProcessDataService, private linkService: LinkService) {
* The process to create
*/
public process: Process;
/**
* The parameter values to use to start the process
*/
public parameters: ProcessParameter[] = [];
/**
* Optional files that are used as parameter values
*/
public files: File[] = [];
/**
* Contains the missing parameters on submission
*/
public missingParameters = [];
constructor(
private scriptService: ScriptDataService,
private notificationsService: NotificationsService,
private translationService: TranslateService,
private requestService: RequestService,
private router: Router) {
} }
ngOnInit(): void { ngOnInit() {
this.process = new Process(); const id = this.route.snapshot.queryParams.id;
} if (id) {
this.fromExisting$ = this.processService.findById(id).pipe(getFirstSucceededRemoteDataPayload());
/** this.script$ = this.fromExisting$.pipe(
* Validates the form, sets the parameters to correct values and invokes the script with the correct parameters map((process: Process) => this.linkService.resolveLink<Process>(process, followLink('script'))),
* @param form switchMap((process: Process) => process.script),
*/ getFirstSucceededRemoteDataPayload()
submitForm(form: NgForm) { );
if (!this.validateForm(form) || this.isRequiredMissing()) {
return;
} }
const stringParameters: ProcessParameter[] = this.parameters.map((parameter: ProcessParameter) => {
return {
name: parameter.name,
value: this.checkValue(parameter)
};
}
);
this.scriptService.invoke(this.selectedScript.id, stringParameters, this.files)
.pipe(take(1))
.subscribe((requestEntry: RequestEntry) => {
if (requestEntry.response.isSuccessful) {
const title = this.translationService.get('process.new.notification.success.title');
const content = this.translationService.get('process.new.notification.success.content');
this.notificationsService.success(title, content)
this.sendBack();
} else {
const title = this.translationService.get('process.new.notification.error.title');
const content = this.translationService.get('process.new.notification.error.content');
this.notificationsService.error(title, content)
}
})
}
/**
* Checks whether the parameter values are files
* Replaces file parameters by strings and stores the files in a separate list
* @param processParameter The parameter value to check
*/
private checkValue(processParameter: ProcessParameter): string {
if (typeof processParameter.value === 'object') {
this.files = [...this.files, processParameter.value];
return processParameter.value.name;
}
return processParameter.value;
}
/**
* Validates the form
* Returns false if the form is invalid
* Returns true if the form is valid
* @param form The NgForm object to validate
*/
private validateForm(form: NgForm) {
if (form.invalid) {
Object.keys(form.controls).forEach((key) => {
form.controls[key].markAsDirty();
});
return false;
}
return true;
}
private isRequiredMissing() {
const setParams: string[] = this.parameters
.map((param) => param.name);
const requiredParams: ScriptParameter[] = this.selectedScript.parameters.filter((param) => param.mandatory)
for (const rp of requiredParams) {
if (!setParams.includes(rp.name)) {
this.missingParameters.push(rp.name);
}
}
return this.missingParameters.length > 0;
}
private sendBack() {
this.requestService.removeByHrefSubstring('/processes');
/* should subscribe on the previous method to know the action is finished and then navigate,
will fix this when the removeByHrefSubstring changes are merged */
this.router.navigateByUrl('/processes');
} }
} }
export function controlContainerFactory(controlContainer?: ControlContainer) {
return controlContainer;
}

View File

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

View File

@@ -2,20 +2,21 @@ import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { ProcessPageRoutingModule } from './process-page-routing.module'; import { ProcessPageRoutingModule } from './process-page-routing.module';
import { NewProcessComponent } from './new/new-process.component'; import { NewProcessComponent } from './new/new-process.component';
import { ScriptsSelectComponent } from './new/scripts-select/scripts-select.component'; import { ScriptsSelectComponent } from './form/scripts-select/scripts-select.component';
import { ScriptHelpComponent } from './new/script-help/script-help.component'; import { ScriptHelpComponent } from './form/script-help/script-help.component';
import { ParameterSelectComponent } from './new/process-parameters/parameter-select/parameter-select.component'; import { ParameterSelectComponent } from './form/process-parameters/parameter-select/parameter-select.component';
import { ProcessParametersComponent } from './new/process-parameters/process-parameters.component'; import { ProcessParametersComponent } from './form/process-parameters/process-parameters.component';
import { StringValueInputComponent } from './new/process-parameters/parameter-value-input/string-value-input/string-value-input.component'; import { StringValueInputComponent } from './form/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 { ParameterValueInputComponent } from './form/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 { FileValueInputComponent } from './form/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 { BooleanValueInputComponent } from './form/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'; import { DateValueInputComponent } from './form/process-parameters/parameter-value-input/date-value-input/date-value-input.component';
import { ProcessOverviewComponent } from './overview/process-overview.component'; import { ProcessOverviewComponent } from './overview/process-overview.component';
import { ProcessDetailComponent } from './detail/process-detail.component'; import { ProcessDetailComponent } from './detail/process-detail.component';
import { ProcessDetailFieldComponent } from './detail/process-detail-field/process-detail-field.component'; import { ProcessDetailFieldComponent } from './detail/process-detail-field/process-detail-field.component';
import { ProcessBreadcrumbsService } from './process-breadcrumbs.service'; import { ProcessBreadcrumbsService } from './process-breadcrumbs.service';
import { ProcessBreadcrumbResolver } from './process-breadcrumb.resolver'; import { ProcessBreadcrumbResolver } from './process-breadcrumb.resolver';
import { ProcessFormComponent } from './form/process-form.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -35,7 +36,8 @@ import { ProcessBreadcrumbResolver } from './process-breadcrumb.resolver';
DateValueInputComponent, DateValueInputComponent,
ProcessOverviewComponent, ProcessOverviewComponent,
ProcessDetailComponent, ProcessDetailComponent,
ProcessDetailFieldComponent ProcessDetailFieldComponent,
ProcessFormComponent
], ],
providers: [ providers: [
ProcessBreadcrumbResolver, ProcessBreadcrumbResolver,

View File

@@ -13,6 +13,7 @@ import { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { SCRIPT } from '../scripts/script.resource-type'; import { SCRIPT } from '../scripts/script.resource-type';
import { Script } from '../scripts/script.model'; import { Script } from '../scripts/script.model';
import { PaginatedList } from '../../core/data/paginated-list';
/** /**
* Object representing a process * Object representing a process
@@ -83,7 +84,7 @@ export class Process implements CacheableObject {
/** /**
* The Script that created this Process * The Script that created this Process
* Will be undefined unless the owningCollection {@link HALLink} has been resolved. * Will be undefined unless the script {@link HALLink} has been resolved.
*/ */
@link(SCRIPT) @link(SCRIPT)
script?: Observable<RemoteData<Script>>; script?: Observable<RemoteData<Script>>;

View File

@@ -22,7 +22,11 @@ import { RouterLinkDirectiveStub } from './router-link-directive.stub';
MySimpleItemActionComponent, MySimpleItemActionComponent,
RouterLinkDirectiveStub, RouterLinkDirectiveStub,
NgComponentOutletDirectiveStub NgComponentOutletDirectiveStub
], schemas: [ ],
exports: [
QueryParamsDirectiveStub
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA
] ]
}) })

View File

@@ -1971,6 +1971,8 @@
"process.detail.status" : "Status", "process.detail.status" : "Status",
"process.detail.create" : "Create similar process",
"process.overview.table.finish" : "Finish time", "process.overview.table.finish" : "Finish time",