mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #636 from atmire/scripts-processes
Scripts & processes admin UI
This commit is contained in:
@@ -101,7 +101,7 @@
|
|||||||
"moment": "^2.22.1",
|
"moment": "^2.22.1",
|
||||||
"morgan": "^1.9.1",
|
"morgan": "^1.9.1",
|
||||||
"ng-mocks": "^8.1.0",
|
"ng-mocks": "^8.1.0",
|
||||||
"ng2-file-upload": "1.2.1",
|
"ng2-file-upload": "1.4.0",
|
||||||
"ng2-nouislider": "^1.8.2",
|
"ng2-nouislider": "^1.8.2",
|
||||||
"ngx-bootstrap": "^5.3.2",
|
"ngx-bootstrap": "^5.3.2",
|
||||||
"ngx-infinite-scroll": "6.0.1",
|
"ngx-infinite-scroll": "6.0.1",
|
||||||
|
@@ -145,11 +145,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
this.modalService.open(CreateItemParentSelectorComponent);
|
this.modalService.open(CreateItemParentSelectorComponent);
|
||||||
}
|
}
|
||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
// model: {
|
},
|
||||||
// type: MenuItemType.LINK,
|
{
|
||||||
// text: 'menu.section.new_item',
|
id: 'new_process',
|
||||||
// link: '/submit'
|
parentID: 'new',
|
||||||
// } as LinkMenuItemModel,
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.new_process',
|
||||||
|
link: '/processes/new'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new_item_version',
|
id: 'new_item_version',
|
||||||
@@ -439,6 +445,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
icon: 'cogs',
|
icon: 'cogs',
|
||||||
index: 9
|
index: 9
|
||||||
},
|
},
|
||||||
|
/* Processes */
|
||||||
|
{
|
||||||
|
id: 'processes',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.processes',
|
||||||
|
link: '/processes'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'terminal',
|
||||||
|
index: 10
|
||||||
|
},
|
||||||
/* Workflow */
|
/* Workflow */
|
||||||
{
|
{
|
||||||
id: 'workflow',
|
id: 'workflow',
|
||||||
|
@@ -114,6 +114,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
path: PROFILE_MODULE_PATH,
|
path: PROFILE_MODULE_PATH,
|
||||||
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
||||||
},
|
},
|
||||||
|
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
@@ -1,21 +1,38 @@
|
|||||||
import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
describe('I18nBreadcrumbResolver', () => {
|
describe('I18nBreadcrumbResolver', () => {
|
||||||
describe('resolve', () => {
|
describe('resolve', () => {
|
||||||
let resolver: I18nBreadcrumbResolver;
|
let resolver: I18nBreadcrumbResolver;
|
||||||
let i18nBreadcrumbService: any;
|
let i18nBreadcrumbService: any;
|
||||||
let i18nKey: string;
|
let i18nKey: string;
|
||||||
let path: string;
|
let route: any;
|
||||||
|
let parentSegment;
|
||||||
|
let segment;
|
||||||
|
let expectedPath;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
i18nKey = 'example.key';
|
i18nKey = 'example.key';
|
||||||
path = 'rest.com/path/to/breadcrumb';
|
parentSegment = 'path';
|
||||||
|
segment = 'breadcrumb';
|
||||||
|
route = {
|
||||||
|
data: { breadcrumbKey: i18nKey },
|
||||||
|
routeConfig: {
|
||||||
|
path: segment
|
||||||
|
},
|
||||||
|
parent: {
|
||||||
|
routeConfig: {
|
||||||
|
path: parentSegment
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
};
|
||||||
|
expectedPath = new URLCombiner(parentSegment, segment).toString();
|
||||||
i18nBreadcrumbService = {};
|
i18nBreadcrumbService = {};
|
||||||
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService);
|
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the breadcrumb config', () => {
|
it('should resolve the breadcrumb config', () => {
|
||||||
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path], pathFromRoot: [{ url: [path] }] } as any, {} as any);
|
const resolvedConfig = resolver.resolve(route, {} as any);
|
||||||
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path };
|
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath };
|
||||||
expect(resolvedConfig).toEqual(expectedConfig);
|
expect(resolvedConfig).toEqual(expectedConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
|
||||||
import { hasNoValue } from '../../shared/empty.util';
|
import { hasNoValue } from '../../shared/empty.util';
|
||||||
|
import { currentPathFromSnapshot } from '../../shared/utils/route.utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
||||||
@@ -25,17 +26,7 @@ export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>>
|
|||||||
if (hasNoValue(key)) {
|
if (hasNoValue(key)) {
|
||||||
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
|
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
|
||||||
}
|
}
|
||||||
const fullPath = this.getResolvedUrl(route);
|
const fullPath = currentPathFromSnapshot(route);
|
||||||
return { provider: this.breadcrumbService, key: key, url: fullPath };
|
return { provider: this.breadcrumbService, key: key, url: fullPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the full URL of an ActivatedRouteSnapshot
|
|
||||||
* @param route
|
|
||||||
*/
|
|
||||||
getResolvedUrl(route: ActivatedRouteSnapshot): string {
|
|
||||||
return route.pathFromRoot
|
|
||||||
.map((v) => v.url.map((segment) => segment.toString()).join('/'))
|
|
||||||
.join('/');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||||
|
|
||||||
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
|
||||||
@@ -138,6 +137,11 @@ import { VersionDataService } from './data/version-data.service';
|
|||||||
import { VersionHistoryDataService } from './data/version-history-data.service';
|
import { VersionHistoryDataService } from './data/version-history-data.service';
|
||||||
import { Version } from './shared/version.model';
|
import { Version } from './shared/version.model';
|
||||||
import { VersionHistory } from './shared/version-history.model';
|
import { VersionHistory } from './shared/version-history.model';
|
||||||
|
import { Script } from '../process-page/scripts/script.model';
|
||||||
|
import { Process } from '../process-page/processes/process.model';
|
||||||
|
import { ProcessDataService } from './data/processes/process-data.service';
|
||||||
|
import { ScriptDataService } from './data/processes/script-data.service';
|
||||||
|
import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service';
|
||||||
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
||||||
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||||
import { Registration } from './shared/registration.model';
|
import { Registration } from './shared/registration.model';
|
||||||
@@ -258,6 +262,9 @@ const PROVIDERS = [
|
|||||||
LicenseDataService,
|
LicenseDataService,
|
||||||
ItemTypeDataService,
|
ItemTypeDataService,
|
||||||
WorkflowActionDataService,
|
WorkflowActionDataService,
|
||||||
|
ProcessDataService,
|
||||||
|
ScriptDataService,
|
||||||
|
ProcessFilesResponseParsingService,
|
||||||
MetadataSchemaDataService,
|
MetadataSchemaDataService,
|
||||||
MetadataFieldDataService,
|
MetadataFieldDataService,
|
||||||
TokenResponseParsingService,
|
TokenResponseParsingService,
|
||||||
@@ -309,6 +316,8 @@ export const models =
|
|||||||
ItemType,
|
ItemType,
|
||||||
ExternalSource,
|
ExternalSource,
|
||||||
ExternalSourceEntry,
|
ExternalSourceEntry,
|
||||||
|
Script,
|
||||||
|
Process,
|
||||||
Version,
|
Version,
|
||||||
VersionHistory,
|
VersionHistory,
|
||||||
WorkflowAction,
|
WorkflowAction,
|
||||||
|
41
src/app/core/data/process-files-response-parsing.service.ts
Normal file
41
src/app/core/data/process-files-response-parsing.service.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { GenericSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||||
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a GenericSuccessResponse
|
||||||
|
* containing a PaginatedList of a process's output files
|
||||||
|
*/
|
||||||
|
export class ProcessFilesResponseParsingService implements ResponseParsingService {
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
const payload = data.payload;
|
||||||
|
|
||||||
|
let page;
|
||||||
|
if (isNotEmpty(payload._embedded) && isNotEmpty(Object.keys(payload._embedded))) {
|
||||||
|
const bitstreams = new DSpaceSerializer(Bitstream).deserializeArray(payload._embedded[Object.keys(payload._embedded)[0]]);
|
||||||
|
|
||||||
|
if (isNotEmpty(bitstreams)) {
|
||||||
|
page = new PaginatedList(Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: bitstreams.length,
|
||||||
|
totalElements: bitstreams.length,
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1
|
||||||
|
}), bitstreams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(page)) {
|
||||||
|
page = new PaginatedList(new PageInfo(), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GenericSuccessResponse(page, data.statusCode, data.statusText);
|
||||||
|
}
|
||||||
|
}
|
70
src/app/core/data/processes/process-data.service.ts
Normal file
70
src/app/core/data/processes/process-data.service.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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';
|
||||||
|
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 { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
|
||||||
|
import { Process } from '../../../process-page/processes/process.model';
|
||||||
|
import { dataService } from '../../cache/builders/build-decorators';
|
||||||
|
import { PROCESS } from '../../../process-page/processes/process.resource-type';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { ProcessFilesRequest, RestRequest } from '../request.models';
|
||||||
|
import { configureRequest, filterSuccessfulResponses } from '../../shared/operators';
|
||||||
|
import { GenericSuccessResponse } from '../../cache/response.models';
|
||||||
|
import { PaginatedList } from '../paginated-list';
|
||||||
|
import { Bitstream } from '../../shared/bitstream.model';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(PROCESS)
|
||||||
|
export class ProcessDataService extends DataService<Process> {
|
||||||
|
protected linkPath = 'processes';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<Process>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for a process his files
|
||||||
|
* @param processId The ID of the process
|
||||||
|
*/
|
||||||
|
getFilesEndpoint(processId: string): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
switchMap((href) => this.halService.getEndpoint('files', `${href}/${processId}`))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a process his output files
|
||||||
|
* @param processId The ID of the process
|
||||||
|
*/
|
||||||
|
getFiles(processId: string): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||||
|
const request$ = this.getFilesEndpoint(processId).pipe(
|
||||||
|
map((href) => new ProcessFilesRequest(this.requestService.generateRequestId(), href)),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
);
|
||||||
|
const requestEntry$ = request$.pipe(
|
||||||
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
|
);
|
||||||
|
const payload$ = requestEntry$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((response: GenericSuccessResponse<PaginatedList<Bitstream>>) => response.payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rdbService.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
|
}
|
||||||
|
}
|
61
src/app/core/data/processes/script-data.service.ts
Normal file
61
src/app/core/data/processes/script-data.service.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DataService } from '../data.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
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 { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
|
||||||
|
import { Script } from '../../../process-page/scripts/script.model';
|
||||||
|
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
|
||||||
|
import { find, map, switchMap } from 'rxjs/operators';
|
||||||
|
import { URLCombiner } from '../../url-combiner/url-combiner';
|
||||||
|
import { MultipartPostRequest, RestRequest } from '../request.models';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RequestEntry } from '../request.reducer';
|
||||||
|
import { dataService } from '../../cache/builders/build-decorators';
|
||||||
|
import { SCRIPT } from '../../../process-page/scripts/script.resource-type';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(SCRIPT)
|
||||||
|
export class ScriptDataService extends DataService<Script> {
|
||||||
|
protected linkPath = 'scripts';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<Script>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public invoke(scriptName: string, parameters: ProcessParameter[], files: File[]): Observable<RequestEntry> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
map((endpoint: string) => new URLCombiner(endpoint, scriptName, 'processes').toString()),
|
||||||
|
map((endpoint: string) => {
|
||||||
|
const body = this.getInvocationFormData(parameters, files);
|
||||||
|
return new MultipartPostRequest(requestId, endpoint, body)
|
||||||
|
}),
|
||||||
|
map((request: RestRequest) => this.requestService.configure(request)),
|
||||||
|
switchMap(() => this.requestService.getByUUID(requestId)),
|
||||||
|
find((request: RequestEntry) => request.completed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@@ -11,12 +11,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
|||||||
|
|
||||||
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||||
import {
|
import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction, ResetResponseTimestampsAction } from './request.actions';
|
||||||
RequestActionTypes,
|
|
||||||
RequestCompleteAction,
|
|
||||||
RequestExecuteAction,
|
|
||||||
ResetResponseTimestampsAction
|
|
||||||
} from './request.actions';
|
|
||||||
import { RequestError, RestRequest } from './request.models';
|
import { RequestError, RestRequest } from './request.models';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
@@ -42,12 +37,12 @@ export class RequestEffects {
|
|||||||
filter((entry: RequestEntry) => hasValue(entry)),
|
filter((entry: RequestEntry) => hasValue(entry)),
|
||||||
map((entry: RequestEntry) => entry.request),
|
map((entry: RequestEntry) => entry.request),
|
||||||
flatMap((request: RestRequest) => {
|
flatMap((request: RestRequest) => {
|
||||||
let body;
|
let body = request.body;
|
||||||
if (isNotEmpty(request.body)) {
|
if (isNotEmpty(request.body) && !request.isMultipart) {
|
||||||
const serializer = new DSpaceSerializer(getClassForType(request.body.type));
|
const serializer = new DSpaceSerializer(getClassForType(request.body.type));
|
||||||
body = serializer.serialize(request.body);
|
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)),
|
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
|
||||||
addToResponseCacheAndCompleteAction(request),
|
addToResponseCacheAndCompleteAction(request),
|
||||||
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(
|
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(
|
||||||
|
@@ -18,6 +18,7 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||||
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
|
import { ProcessFilesResponseParsingService } from './process-files-response-parsing.service';
|
||||||
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -31,6 +32,8 @@ export enum IdentifierType {
|
|||||||
export abstract class RestRequest {
|
export abstract class RestRequest {
|
||||||
public responseMsToLive = 10 * 1000;
|
public responseMsToLive = 10 * 1000;
|
||||||
public forceBypassCache = false;
|
public forceBypassCache = false;
|
||||||
|
public isMultipart = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
@@ -73,6 +76,21 @@ export class PostRequest extends RestRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request representing a multipart post request
|
||||||
|
*/
|
||||||
|
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 {
|
export class PutRequest extends RestRequest {
|
||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
@@ -208,6 +226,15 @@ export class MappedCollectionsRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to fetch the files of a process
|
||||||
|
*/
|
||||||
|
export class ProcessFilesRequest extends GetRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return ProcessFilesResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ConfigRequest extends GetRequest {
|
export class ConfigRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
||||||
super(uuid, href, null, options);
|
super(uuid, href, null, options);
|
||||||
|
@@ -69,10 +69,12 @@ export class DSpaceRESTv2Service {
|
|||||||
* an optional body for the request
|
* an optional body for the request
|
||||||
* @param options
|
* @param options
|
||||||
* the HttpOptions object
|
* the HttpOptions object
|
||||||
|
* @param isMultipart
|
||||||
|
* true when this concerns a multipart request
|
||||||
* @return {Observable<string>}
|
* @return {Observable<string>}
|
||||||
* An Observable<string> containing the response from the server
|
* 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 = {};
|
const requestOptions: HttpOptions = {};
|
||||||
requestOptions.body = body;
|
requestOptions.body = body;
|
||||||
if (method === RestRequestMethod.POST && isNotEmpty(body) && isNotEmpty(body.name)) {
|
if (method === RestRequestMethod.POST && isNotEmpty(body) && isNotEmpty(body.name)) {
|
||||||
@@ -98,7 +100,7 @@ export class DSpaceRESTv2Service {
|
|||||||
requestOptions.withCredentials = options.withCredentials;
|
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
|
// 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);
|
requestOptions.headers = requestOptions.headers.set('Content-Type', DEFAULT_CONTENT_TYPE);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
<h4 class="mt-4">{{title | translate}}</h4>
|
||||||
|
<ng-content></ng-content>
|
@@ -0,0 +1,38 @@
|
|||||||
|
import { ProcessDetailFieldComponent } from './process-detail-field.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('ProcessDetailFieldComponent', () => {
|
||||||
|
let component: ProcessDetailFieldComponent;
|
||||||
|
let fixture: ComponentFixture<ProcessDetailFieldComponent>;
|
||||||
|
|
||||||
|
let title;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
title = 'fake.title.message';
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProcessDetailFieldComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProcessDetailFieldComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.title = title;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the given title', () => {
|
||||||
|
const header = fixture.debugElement.query(By.css('h4')).nativeElement;
|
||||||
|
expect(header.textContent).toContain(title);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-process-detail-field',
|
||||||
|
templateUrl: './process-detail-field.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A component displaying a single detail about a DSpace Process
|
||||||
|
*/
|
||||||
|
export class ProcessDetailFieldComponent {
|
||||||
|
/**
|
||||||
|
* I18n message for the header
|
||||||
|
*/
|
||||||
|
@Input() title: string;
|
||||||
|
}
|
42
src/app/process-page/detail/process-detail.component.html
Normal file
42
src/app/process-page/detail/process-detail.component.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
||||||
|
<div class="d-flex">
|
||||||
|
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-light" [routerLink]="'/processes/new'" [queryParams]="{id: process?.processId}">{{'process.detail.create' | translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||||
|
<div>{{ process?.scriptName }}</div>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
|
||||||
|
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments" [title]="'process.detail.arguments'">
|
||||||
|
<div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</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'">
|
||||||
|
<ds-file-download-link *ngFor="let file of files; let last=last;" [href]="file?._links?.content?.href" [download]="getFileName(file)">
|
||||||
|
<span>{{getFileName(file)}}</span>
|
||||||
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
|
</ds-file-download-link>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate">
|
||||||
|
<div>{{ process.startTime }}</div>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
|
||||||
|
<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>-->
|
||||||
|
<!--</ds-process-detail-field>-->
|
||||||
|
|
||||||
|
<a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||||
|
</div>
|
107
src/app/process-page/detail/process-detail.component.spec.ts
Normal file
107
src/app/process-page/detail/process-detail.component.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { ProcessDetailComponent } from './process-detail.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
|
||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
|
||||||
|
describe('ProcessDetailComponent', () => {
|
||||||
|
let component: ProcessDetailComponent;
|
||||||
|
let fixture: ComponentFixture<ProcessDetailComponent>;
|
||||||
|
|
||||||
|
let processService: ProcessDataService;
|
||||||
|
let nameService: DSONameService;
|
||||||
|
|
||||||
|
let process: Process;
|
||||||
|
let fileName: string;
|
||||||
|
let files: Bitstream[];
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
process = Object.assign(new Process(), {
|
||||||
|
processId: 1,
|
||||||
|
scriptName: 'script-name',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: '-f',
|
||||||
|
value: 'file.xml'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '-i',
|
||||||
|
value: 'identifier'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
fileName = 'fake-file-name';
|
||||||
|
files = [
|
||||||
|
Object.assign(new Bitstream(), {
|
||||||
|
sizeBytes: 10000,
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
value: fileName,
|
||||||
|
language: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
content: { href: 'file-selflink' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
processService = jasmine.createSpyObj('processService', {
|
||||||
|
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files))
|
||||||
|
});
|
||||||
|
nameService = jasmine.createSpyObj('nameService', {
|
||||||
|
getName: fileName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } },
|
||||||
|
{ provide: ProcessDataService, useValue: processService },
|
||||||
|
{ provide: DSONameService, useValue: nameService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProcessDetailComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the script\'s name', () => {
|
||||||
|
const name = fixture.debugElement.query(By.css('#process-name')).nativeElement;
|
||||||
|
expect(name.textContent).toContain(process.scriptName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the process\'s parameters', () => {
|
||||||
|
const args = fixture.debugElement.query(By.css('#process-arguments')).nativeElement;
|
||||||
|
process.parameters.forEach((param) => {
|
||||||
|
expect(args.textContent).toContain(`${param.name} ${param.value}`)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the process\'s output files', () => {
|
||||||
|
const processFiles = fixture.debugElement.query(By.css('#process-files')).nativeElement;
|
||||||
|
expect(processFiles.textContent).toContain(fileName);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
69
src/app/process-page/detail/process-detail.component.ts
Normal file
69
src/app/process-page/detail/process-detail.component.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { getFirstSucceededRemoteDataPayload, redirectToPageNotFoundOn404 } from '../../core/shared/operators';
|
||||||
|
import { AlertType } from '../../shared/alert/aletr-type';
|
||||||
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-process-detail',
|
||||||
|
templateUrl: './process-detail.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A component displaying detailed information about a DSpace Process
|
||||||
|
*/
|
||||||
|
export class ProcessDetailComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AlertType enumeration
|
||||||
|
* @type {AlertType}
|
||||||
|
*/
|
||||||
|
public AlertTypeEnum = AlertType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Process's Remote Data
|
||||||
|
*/
|
||||||
|
processRD$: Observable<RemoteData<Process>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Process's Output Files
|
||||||
|
*/
|
||||||
|
filesRD$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
|
protected processService: ProcessDataService,
|
||||||
|
protected nameService: DSONameService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize component properties
|
||||||
|
* Display a 404 if the process doesn't exist
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.processRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.process as RemoteData<Process>),
|
||||||
|
redirectToPageNotFoundOn404(this.router)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.filesRD$ = this.processRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((process: Process) => this.processService.getFiles(process.processId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of a bitstream
|
||||||
|
* @param bitstream
|
||||||
|
*/
|
||||||
|
getFileName(bitstream: Bitstream) {
|
||||||
|
return this.nameService.getName(bitstream);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
src/app/process-page/form/process-form.component.html
Normal file
25
src/app/process-page/form/process-form.component.html
Normal 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="mt-3 alert alert-danger validation-error">
|
||||||
|
{{'process.new.parameter.required.missing' | translate}}
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let missing of missingParameters">{{missing}}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
86
src/app/process-page/form/process-form.component.spec.ts
Normal file
86
src/app/process-page/form/process-form.component.spec.ts
Normal 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('ProcessFormComponent', () => {
|
||||||
|
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: jasmine.createSpyObj('requestService', ['removeBySubstring']) },
|
||||||
|
{ 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({ controls: {} } as any);
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
151
src/app/process-page/form/process-form.component.ts
Normal file
151
src/app/process-page/form/process-form.component.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
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[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message key for the header of the form
|
||||||
|
*/
|
||||||
|
@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) {
|
||||||
|
let valid = true;
|
||||||
|
Object.keys(form.controls).forEach((key) => {
|
||||||
|
if (form.controls[key].invalid) {
|
||||||
|
form.controls[key].markAsDirty();
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isRequiredMissing() {
|
||||||
|
this.missingParameters = [];
|
||||||
|
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;
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="form-row mb-2 mx-0">
|
||||||
|
<select id="process-parameters"
|
||||||
|
class="form-control col"
|
||||||
|
name="parameter-{{index}}"
|
||||||
|
[(ngModel)]="selectedParameter"
|
||||||
|
#param="ngModel">
|
||||||
|
<option [ngValue]="undefined">Add a parameter...</option>
|
||||||
|
<option *ngFor="let param of parameters" [ngValue]="param.name">
|
||||||
|
{{param.nameLong || param.name}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<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>
|
||||||
|
<span *ngIf="!removable" class="col-1"></span>
|
||||||
|
</div>
|
||||||
|
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ParameterSelectComponent } from './parameter-select.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ScriptParameter } from '../../../scripts/script-parameter.model';
|
||||||
|
import { ScriptParameterType } from '../../../scripts/script-parameter-type.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('ParameterSelectComponent', () => {
|
||||||
|
let component: ParameterSelectComponent;
|
||||||
|
let fixture: ComponentFixture<ParameterSelectComponent>;
|
||||||
|
let scriptParams: ScriptParameter[];
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
scriptParams = [
|
||||||
|
Object.assign(
|
||||||
|
new ScriptParameter(),
|
||||||
|
{
|
||||||
|
name: '-a',
|
||||||
|
type: ScriptParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Object.assign(
|
||||||
|
new ScriptParameter(),
|
||||||
|
{
|
||||||
|
name: '-f',
|
||||||
|
type: ScriptParameterType.FILE
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [FormsModule],
|
||||||
|
declarations: [ParameterSelectComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ParameterSelectComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
component.parameters = scriptParams;
|
||||||
|
component.removable = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the remove button when removable', () => {
|
||||||
|
component.removable = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const button = fixture.debugElement.query(By.css('button.remove-button'));
|
||||||
|
expect(button).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide the remove button when not removable', () => {
|
||||||
|
component.removable = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const button = fixture.debugElement.query(By.css('button.remove-button'));
|
||||||
|
expect(button).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,89 @@
|
|||||||
|
import { Component, EventEmitter, Input, Optional, Output } from '@angular/core';
|
||||||
|
import { ProcessParameter } from '../../../processes/process-parameter.model';
|
||||||
|
import { ScriptParameter } from '../../../scripts/script-parameter.model';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to select a single parameter for a process
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-parameter-select',
|
||||||
|
templateUrl: './parameter-select.component.html',
|
||||||
|
styleUrls: ['./parameter-select.component.scss'],
|
||||||
|
viewProviders: [{
|
||||||
|
provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]]
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
export class ParameterSelectComponent {
|
||||||
|
@Input() index: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current parameter value of the selected parameter
|
||||||
|
*/
|
||||||
|
@Input() parameterValue: ProcessParameter = new ProcessParameter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available script parameters for the script
|
||||||
|
*/
|
||||||
|
@Input() parameters: ScriptParameter[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not this selected parameter can be removed from the list
|
||||||
|
*/
|
||||||
|
@Input() removable: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the parameter value when it's removed
|
||||||
|
*/
|
||||||
|
@Output() removeParameter: EventEmitter<ProcessParameter> = new EventEmitter<ProcessParameter>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the updated parameter value when it changes
|
||||||
|
*/
|
||||||
|
@Output() changeParameter: EventEmitter<ProcessParameter> = new EventEmitter<ProcessParameter>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the script parameter based on the currently selected name
|
||||||
|
*/
|
||||||
|
get selectedScriptParameter(): ScriptParameter {
|
||||||
|
return this.parameters.find((parameter: ScriptParameter) => parameter.name === this.selectedParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the currently selected parameter name
|
||||||
|
*/
|
||||||
|
get selectedParameter(): string {
|
||||||
|
return this.parameterValue ? this.parameterValue.name : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the currently selected parameter based on the provided parameter name
|
||||||
|
* Emits the new value from the changeParameter output
|
||||||
|
* @param value The parameter name to set
|
||||||
|
*/
|
||||||
|
set selectedParameter(value: string) {
|
||||||
|
this.parameterValue.name = value;
|
||||||
|
this.selectedParameterValue = undefined;
|
||||||
|
this.changeParameter.emit(this.parameterValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently selected parameter value
|
||||||
|
*/
|
||||||
|
get selectedParameterValue(): any {
|
||||||
|
return this.parameterValue ? this.parameterValue.value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the currently selected value for the parameter
|
||||||
|
* Emits the new value from the changeParameter output
|
||||||
|
* @param value The parameter value to set
|
||||||
|
*/
|
||||||
|
set selectedParameterValue(value: any) {
|
||||||
|
this.parameterValue.value = value;
|
||||||
|
this.changeParameter.emit(this.parameterValue);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1 @@
|
|||||||
|
<input type="hidden" value="true" name="boolean-value-{{index}}" id="boolean-value-{{index}}"/>
|
@@ -0,0 +1,32 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BooleanValueInputComponent } from './boolean-value-input.component';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('BooleanValueInputComponent', () => {
|
||||||
|
let component: BooleanValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<BooleanValueInputComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [BooleanValueInputComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BooleanValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
spyOn(component.updateValue, 'emit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit true onInit', () => {
|
||||||
|
expect(component.updateValue.emit).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component, OnInit, Optional } from '@angular/core';
|
||||||
|
import { ValueInputComponent } from '../value-input.component';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the value of a boolean parameter
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-boolean-value-input',
|
||||||
|
templateUrl: './boolean-value-input.component.html',
|
||||||
|
styleUrls: ['./boolean-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class BooleanValueInputComponent extends ValueInputComponent<boolean> implements OnInit {
|
||||||
|
ngOnInit() {
|
||||||
|
this.updateValue.emit(true)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<input required #string="ngModel" type="text" class="form-control" name="date-value-{{index}}" id="date-value-{{index}}" [ngModel]="value" (ngModelChange)="setValue($event)"/>
|
||||||
|
<div *ngIf="string.invalid && (string.dirty || string.touched)"
|
||||||
|
class="alert alert-danger validation-error">
|
||||||
|
<div *ngIf="string.errors.required">
|
||||||
|
{{'process.new.parameter.string.required' | translate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DateValueInputComponent } from './date-value-input.component';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('DateValueInputComponent', () => {
|
||||||
|
let component: DateValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<DateValueInputComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [DateValueInputComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DateValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was left untouched but left empty', () => {
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a validation error if the input field was touched but left empty', fakeAsync(() => {
|
||||||
|
component.value = '';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was touched but not left empty', fakeAsync(() => {
|
||||||
|
component.value = 'testValue';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
}));
|
||||||
|
});
|
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, Input, Optional } from '@angular/core';
|
||||||
|
import { ValueInputComponent } from '../value-input.component';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the user inputted value of a date parameter
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-date-value-input',
|
||||||
|
templateUrl: './date-value-input.component.html',
|
||||||
|
styleUrls: ['./date-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class DateValueInputComponent extends ValueInputComponent<string> {
|
||||||
|
/**
|
||||||
|
* The current value of the date string
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial value of the field
|
||||||
|
*/
|
||||||
|
@Input() initialValue;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.value = this.initialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value) {
|
||||||
|
this.value = value;
|
||||||
|
this.updateValue.emit(value)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
<label for="file-upload-{{index}}" class="d-flex align-items-center m-0">
|
||||||
|
<span class="btn btn-light">
|
||||||
|
{{'process.new.parameter.file.upload-button' | translate}}
|
||||||
|
</span>
|
||||||
|
<span class="file-name ml-1">{{fileObject?.name}}</span>
|
||||||
|
</label>
|
||||||
|
<input requireFile #file="ngModel" type="file" name="file-upload-{{index}}" id="file-upload-{{index}}" class="form-control-file d-none" [ngModel]="fileObject" (ngModelChange)="setFile($event)"/>
|
||||||
|
<div *ngIf="file.invalid && (file.dirty || file.touched)"
|
||||||
|
class="alert alert-danger validation-error">
|
||||||
|
<div *ngIf="file.errors.required">
|
||||||
|
{{'process.new.parameter.file.required' | translate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,6 @@
|
|||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
@@ -0,0 +1,57 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormsModule, NgForm, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { FileValueInputComponent } from './file-value-input.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { FileValueAccessorDirective } from '../../../../../shared/utils/file-value-accessor.directive';
|
||||||
|
import { FileValidator } from '../../../../../shared/utils/require-file.validator';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('FileValueInputComponent', () => {
|
||||||
|
let component: FileValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<FileValueInputComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [FileValueInputComponent, FileValueAccessorDirective, FileValidator],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FileValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was left untouched but left empty', () => {
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a validation error if the input field was touched but left empty', () => {
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component, Optional } from '@angular/core';
|
||||||
|
import { ValueInputComponent } from '../value-input.component';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the user inputted value of a file parameter
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-file-value-input',
|
||||||
|
templateUrl: './file-value-input.component.html',
|
||||||
|
styleUrls: ['./file-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class FileValueInputComponent extends ValueInputComponent<File> {
|
||||||
|
/**
|
||||||
|
* The current value of the file
|
||||||
|
*/
|
||||||
|
fileObject: File;
|
||||||
|
setFile(files) {
|
||||||
|
this.fileObject = files.length > 0 ? files[0] : undefined;
|
||||||
|
this.updateValue.emit(this.fileObject);
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
@@ -0,0 +1,105 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ParameterValueInputComponent } from './parameter-value-input.component';
|
||||||
|
import { ScriptParameter } from '../../../scripts/script-parameter.model';
|
||||||
|
import { ScriptParameterType } from '../../../scripts/script-parameter-type.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { BooleanValueInputComponent } from './boolean-value-input/boolean-value-input.component';
|
||||||
|
import { StringValueInputComponent } from './string-value-input/string-value-input.component';
|
||||||
|
import { FileValueInputComponent } from './file-value-input/file-value-input.component';
|
||||||
|
import { DateValueInputComponent } from './date-value-input/date-value-input.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { FileValueAccessorDirective } from '../../../../shared/utils/file-value-accessor.directive';
|
||||||
|
import { FileValidator } from '../../../../shared/utils/require-file.validator';
|
||||||
|
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('ParameterValueInputComponent', () => {
|
||||||
|
let component: ParameterValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<ParameterValueInputComponent>;
|
||||||
|
let booleanParameter;
|
||||||
|
let stringParameter;
|
||||||
|
let fileParameter;
|
||||||
|
let dateParameter;
|
||||||
|
let outputParameter;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
booleanParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.BOOLEAN });
|
||||||
|
stringParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.STRING });
|
||||||
|
fileParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.FILE });
|
||||||
|
dateParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.DATE });
|
||||||
|
outputParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.OUTPUT });
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [
|
||||||
|
ParameterValueInputComponent,
|
||||||
|
BooleanValueInputComponent,
|
||||||
|
StringValueInputComponent,
|
||||||
|
FileValueInputComponent,
|
||||||
|
DateValueInputComponent,
|
||||||
|
FileValueAccessorDirective,
|
||||||
|
FileValidator
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ParameterValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.parameter = stringParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a BooleanValueInputComponent when the parameter type is boolean', () => {
|
||||||
|
component.parameter = booleanParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(BooleanValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a StringValueInputComponent when the parameter type is string', () => {
|
||||||
|
component.parameter = stringParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(StringValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a FileValueInputComponent when the parameter type is file', () => {
|
||||||
|
component.parameter = fileParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(FileValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a DateValueInputComponent when the parameter type is date', () => {
|
||||||
|
component.parameter = dateParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(DateValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a StringValueInputComponent when the parameter type is output', () => {
|
||||||
|
component.parameter = outputParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(StringValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { Component, EventEmitter, Input, Optional, Output } from '@angular/core';
|
||||||
|
import { ScriptParameterType } from '../../../scripts/script-parameter-type.model';
|
||||||
|
import { ScriptParameter } from '../../../scripts/script-parameter.model';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders the correct parameter value input based the script parameter's type
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-parameter-value-input',
|
||||||
|
templateUrl: './parameter-value-input.component.html',
|
||||||
|
styleUrls: ['./parameter-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class ParameterValueInputComponent {
|
||||||
|
@Input() index: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current script parameter
|
||||||
|
*/
|
||||||
|
@Input() parameter: ScriptParameter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial value for input
|
||||||
|
*/
|
||||||
|
@Input() initialValue: any;
|
||||||
|
/**
|
||||||
|
* Emits the value of the input when its updated
|
||||||
|
*/
|
||||||
|
@Output() updateValue: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available script parameter types
|
||||||
|
*/
|
||||||
|
parameterTypes = ScriptParameterType;
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<input required #string="ngModel" type="text" name="string-value-{{index}}" class="form-control" id="string-value-{{index}}" [ngModel]="value" (ngModelChange)="setValue($event)"/>
|
||||||
|
<div *ngIf="string.invalid && (string.dirty || string.touched)"
|
||||||
|
class="alert alert-danger validation-error">
|
||||||
|
<div *ngIf="string.errors.required">
|
||||||
|
{{'process.new.parameter.string.required' | translate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,72 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { StringValueInputComponent } from './string-value-input.component';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('StringValueInputComponent', () => {
|
||||||
|
let component: StringValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<StringValueInputComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [StringValueInputComponent],
|
||||||
|
providers: [
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(StringValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was left untouched but left empty', () => {
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a validation error if the input field was touched but left empty', fakeAsync(() => {
|
||||||
|
component.value = '';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was touched but not left empty', fakeAsync(() => {
|
||||||
|
component.value = 'testValue';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
}));
|
||||||
|
});
|
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, Optional, Input } from '@angular/core';
|
||||||
|
import { ValueInputComponent } from '../value-input.component';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the user inputted value of a string parameter
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-string-value-input',
|
||||||
|
templateUrl: './string-value-input.component.html',
|
||||||
|
styleUrls: ['./string-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class StringValueInputComponent extends ValueInputComponent<string> {
|
||||||
|
/**
|
||||||
|
* The current value of the string
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial value of the field
|
||||||
|
*/
|
||||||
|
@Input() initialValue;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.value = this.initialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value) {
|
||||||
|
this.value = value;
|
||||||
|
this.updateValue.emit(value)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
import { EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class that represents value input components
|
||||||
|
*/
|
||||||
|
export abstract class ValueInputComponent<T> {
|
||||||
|
@Input() index: number;
|
||||||
|
/**
|
||||||
|
* Used by the subclasses to emit the value when it's updated
|
||||||
|
*/
|
||||||
|
@Output() updateValue: EventEmitter<T> = new EventEmitter<T>()
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="form-group" *ngIf="script">
|
||||||
|
<label>{{'process.new.select-parameters' | translate}}</label>
|
||||||
|
<ds-parameter-select
|
||||||
|
*ngFor="let value of parameterValues; let i = index; let last = last"
|
||||||
|
[parameters]="script.parameters"
|
||||||
|
[parameterValue]="value"
|
||||||
|
[removable]="!last"
|
||||||
|
[index]="i"
|
||||||
|
(removeParameter)="removeParameter(i)"
|
||||||
|
(changeParameter)="updateParameter($event, i)"></ds-parameter-select>
|
||||||
|
</div>
|
@@ -0,0 +1,64 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProcessParametersComponent } from './process-parameters.component';
|
||||||
|
import { ProcessParameter } from '../../processes/process-parameter.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ParameterSelectComponent } from './parameter-select/parameter-select.component';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { Script } from '../../scripts/script.model';
|
||||||
|
import { ScriptParameter } from '../../scripts/script-parameter.model';
|
||||||
|
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('ProcessParametersComponent', () => {
|
||||||
|
let component: ProcessParametersComponent;
|
||||||
|
let fixture: ComponentFixture<ProcessParametersComponent>;
|
||||||
|
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' }),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ProcessParametersComponent, ParameterSelectComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProcessParametersComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.script = script;
|
||||||
|
component.parameterValues = parameterValues;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a ParameterSelectComponent for each parameter value of the component', () => {
|
||||||
|
const selectComponents = fixture.debugElement.queryAll(By.directive(ParameterSelectComponent));
|
||||||
|
expect(selectComponents.length).toBe(parameterValues.length);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,114 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnChanges, Optional, Output, SimpleChanges } from '@angular/core';
|
||||||
|
import { Script } from '../../scripts/script.model';
|
||||||
|
import { ProcessParameter } from '../../processes/process-parameter.model';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { ScriptParameter } from '../../scripts/script-parameter.model';
|
||||||
|
import { controlContainerFactory } from '../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the selected list of parameters for a script
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-process-parameters',
|
||||||
|
templateUrl: './process-parameters.component.html',
|
||||||
|
styleUrls: ['./process-parameters.component.scss'],
|
||||||
|
viewProviders: [{
|
||||||
|
provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]]
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
export class ProcessParametersComponent implements OnChanges {
|
||||||
|
/**
|
||||||
|
* The currently selected script
|
||||||
|
*/
|
||||||
|
@Input() script: Script;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial parameters on load
|
||||||
|
*/
|
||||||
|
@Input() initialParams: ProcessParameter[];
|
||||||
|
/**
|
||||||
|
* Emits the parameter values when they're updated
|
||||||
|
*/
|
||||||
|
@Output() updateParameters: EventEmitter<ProcessParameter[]> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current parameter values
|
||||||
|
*/
|
||||||
|
parameterValues: ProcessParameter[];
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (hasValue(this.initialParams)) {
|
||||||
|
this.parameterValues = this.initialParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes sure the parameters are reset when the script changes
|
||||||
|
* @param changes
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes.script) {
|
||||||
|
this.initParameters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empties the parameter values
|
||||||
|
* Initializes the first parameter value
|
||||||
|
*/
|
||||||
|
initParameters() {
|
||||||
|
if (hasValue(this.initialParams)) {
|
||||||
|
this.parameterValues = this.initialParams;
|
||||||
|
} else {
|
||||||
|
this.parameterValues = [];
|
||||||
|
this.initializeParameter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a single parameter value using its new value and index
|
||||||
|
* Adds a new parameter when the last of the parameter values is changed
|
||||||
|
* @param processParameter The new value of the parameter
|
||||||
|
* @param index The index of the parameter
|
||||||
|
*/
|
||||||
|
updateParameter(processParameter: ProcessParameter, index: number) {
|
||||||
|
this.parameterValues[index] = processParameter;
|
||||||
|
if (index === this.parameterValues.length - 1) {
|
||||||
|
this.addParameter();
|
||||||
|
}
|
||||||
|
this.updateParameters.emit(this.parameterValues.filter((param: ProcessParameter) => hasValue(param.name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a parameter value from the list
|
||||||
|
* @param index The index of the parameter to remove
|
||||||
|
*/
|
||||||
|
removeParameter(index: number) {
|
||||||
|
this.parameterValues = this.parameterValues.filter((value, i) => i !== index);
|
||||||
|
this.updateParameters.emit(this.parameterValues.filter((param: ProcessParameter) => hasValue(param.name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes parameter values based on the selected script
|
||||||
|
*/
|
||||||
|
initializeParameter() {
|
||||||
|
if (hasValue(this.script)) {
|
||||||
|
this.parameterValues = this.script.parameters
|
||||||
|
.filter((param) => param.mandatory)
|
||||||
|
.map(
|
||||||
|
(parameter: ScriptParameter) => Object.assign(new ProcessParameter(), { name: parameter.name })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.addParameter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an empty parameter value to the end of the list
|
||||||
|
*/
|
||||||
|
addParameter() {
|
||||||
|
this.parameterValues = [...this.parameterValues, new ProcessParameter()];
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="text-secondary">
|
||||||
|
<h3>{{script?.name}}</h3>
|
||||||
|
<span>{{script?.description}}</span>
|
||||||
|
|
||||||
|
<table class="table-borderless mt-3">
|
||||||
|
<tr *ngFor="let param of script?.parameters">
|
||||||
|
<td class="align-top text-nowrap">{{param.name}} {{param.nameLong}}
|
||||||
|
<ng-container *ngTemplateOutlet="type; context: param"></ng-container>
|
||||||
|
</td>
|
||||||
|
<td>{{param.description}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #type let-type="type">
|
||||||
|
<ng-container [ngSwitch]="type">
|
||||||
|
<span *ngSwitchCase="parameterTypes.DATE"><{{'process.new.parameter.type.value' | translate}}></span>
|
||||||
|
<span *ngSwitchCase="parameterTypes.STRING"><{{'process.new.parameter.type.value' | translate}}></span>
|
||||||
|
<span *ngSwitchCase="parameterTypes.OUTPUT"><{{'process.new.parameter.type.value' | translate}}></span>
|
||||||
|
<span *ngSwitchCase="parameterTypes.FILE"><{{'process.new.parameter.type.file' | translate}}></span>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
@@ -0,0 +1,65 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ScriptHelpComponent } from './script-help.component';
|
||||||
|
import { ScriptParameter } from '../../scripts/script-parameter.model';
|
||||||
|
import { Script } from '../../scripts/script.model';
|
||||||
|
import { ScriptParameterType } from '../../scripts/script-parameter-type.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('ScriptHelpComponent', () => {
|
||||||
|
let component: ScriptHelpComponent;
|
||||||
|
let fixture: ComponentFixture<ScriptHelpComponent>;
|
||||||
|
let script;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const param1 = Object.assign(
|
||||||
|
new ScriptParameter(),
|
||||||
|
{name: '-d', description: 'Lorem ipsum dolor sit amet,', type: ScriptParameterType.DATE}
|
||||||
|
);
|
||||||
|
const param2 = Object.assign(
|
||||||
|
new ScriptParameter(),
|
||||||
|
{name: '-f', description: 'consetetur sadipscing elitr', type: ScriptParameterType.BOOLEAN}
|
||||||
|
);
|
||||||
|
script = Object.assign(new Script(), { parameters: [param1, param2] });
|
||||||
|
}
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ ScriptHelpComponent ],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ScriptHelpComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.script = script;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the name and description for each parameter of the script', () => {
|
||||||
|
const rows = fixture.debugElement.queryAll(By.css('tr'));
|
||||||
|
expect(rows.length).toBe(script.parameters.length);
|
||||||
|
script.parameters.forEach((parameter, index) => {
|
||||||
|
expect(rows[index].queryAll(By.css('td'))[0].nativeElement.textContent).toContain(parameter.name);
|
||||||
|
expect(rows[index].queryAll(By.css('td'))[1].nativeElement.textContent.trim()).toEqual(parameter.description);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Script } from '../../scripts/script.model';
|
||||||
|
import { ScriptParameterType } from '../../scripts/script-parameter-type.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Components that represents a help section for the script use and parameters
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-script-help',
|
||||||
|
templateUrl: './script-help.component.html',
|
||||||
|
styleUrls: ['./script-help.component.scss']
|
||||||
|
})
|
||||||
|
export class ScriptHelpComponent {
|
||||||
|
/**
|
||||||
|
* The current script to show the help information for
|
||||||
|
*/
|
||||||
|
@Input() script: Script;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available script parameter types
|
||||||
|
*/
|
||||||
|
parameterTypes = ScriptParameterType;
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
<div class="form-group" *ngIf="scripts$ | async">
|
||||||
|
<label for="process-script">{{'process.new.select-script' | translate}}</label>
|
||||||
|
<select required id="process-script"
|
||||||
|
class="form-control"
|
||||||
|
name="script"
|
||||||
|
[(ngModel)]="selectedScript"
|
||||||
|
#script="ngModel">
|
||||||
|
<option [ngValue]="undefined">{{'process.new.select-script.placeholder' | translate}}</option>
|
||||||
|
<option *ngFor="let script of scripts$ | async" [ngValue]="script.id">
|
||||||
|
{{script.name}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div *ngIf="script.invalid && (script.dirty || script.touched)"
|
||||||
|
class="alert alert-danger validation-error">
|
||||||
|
<div *ngIf="script.errors.required">
|
||||||
|
{{'process.new.select-script.required' | translate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,99 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ScriptsSelectComponent } from './scripts-select.component';
|
||||||
|
import { Script } from '../../scripts/script.model';
|
||||||
|
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||||
|
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
|
|
||||||
|
describe('ScriptsSelectComponent', () => {
|
||||||
|
let component: ScriptsSelectComponent;
|
||||||
|
let fixture: ComponentFixture<ScriptsSelectComponent>;
|
||||||
|
let scriptService;
|
||||||
|
let script1;
|
||||||
|
let script2;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
script1 = new Script();
|
||||||
|
script2 = new Script();
|
||||||
|
scriptService = jasmine.createSpyObj('scriptService',
|
||||||
|
{
|
||||||
|
findAll: createSuccessfulRemoteDataObject$(new PaginatedList(undefined, [script1, script2]))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ScriptsSelectComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: Router, useClass: RouterStub },
|
||||||
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ScriptsSelectComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
(component as any)._selectedScript = new Script();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was left untouched but left empty', () => {
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a validation error if the input field was touched but left empty', fakeAsync(() => {
|
||||||
|
(component as any)._selectedScript.id = '';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
select.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was touched but not left empty', fakeAsync(() => {
|
||||||
|
(component as any)._selectedScript.id = 'testValue';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
select.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
}));
|
||||||
|
});
|
@@ -0,0 +1,106 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Optional, Output } from '@angular/core';
|
||||||
|
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
|
||||||
|
import { Script } from '../../scripts/script.model';
|
||||||
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||||
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../process-form.component';
|
||||||
|
|
||||||
|
const SCRIPT_QUERY_PARAMETER = 'script';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used to select a script
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-scripts-select',
|
||||||
|
templateUrl: './scripts-select.component.html',
|
||||||
|
styleUrls: ['./scripts-select.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class ScriptsSelectComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* Emits the selected script when the selection changes
|
||||||
|
*/
|
||||||
|
@Output() select: EventEmitter<Script> = new EventEmitter<Script>();
|
||||||
|
/**
|
||||||
|
* All available scripts
|
||||||
|
*/
|
||||||
|
scripts$: Observable<Script[]>;
|
||||||
|
private _selectedScript: Script;
|
||||||
|
private routeSub: Subscription;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private scriptService: ScriptDataService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all available scripts
|
||||||
|
* Checks if the route contains a script ID and auto selects this scripts
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.scripts$ = this.scriptService.findAll({ elementsPerPage: Number.MAX_SAFE_INTEGER })
|
||||||
|
.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((paginatedList: PaginatedList<Script>) => paginatedList.page)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.routeSub = this.route.queryParams
|
||||||
|
.pipe(
|
||||||
|
filter((params: Params) => hasNoValue(params.id)),
|
||||||
|
map((params: Params) => params[SCRIPT_QUERY_PARAMETER]),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((id: string) =>
|
||||||
|
this.scripts$
|
||||||
|
.pipe(
|
||||||
|
take(1),
|
||||||
|
map((scripts) =>
|
||||||
|
scripts.find((script) => script.id === id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).subscribe((script: Script) => {
|
||||||
|
this._selectedScript = script;
|
||||||
|
this.select.emit(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the identifier of the selected script
|
||||||
|
*/
|
||||||
|
get selectedScript(): string {
|
||||||
|
return this._selectedScript ? this._selectedScript.id : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the currently selected script by navigating to the correct route using the scripts ID
|
||||||
|
* @param value The identifier of the script
|
||||||
|
*/
|
||||||
|
set selectedScript(value: string) {
|
||||||
|
this.router.navigate([],
|
||||||
|
{
|
||||||
|
queryParams: { [SCRIPT_QUERY_PARAMETER]: value },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set script(value: Script) {
|
||||||
|
this._selectedScript = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.routeSub)) {
|
||||||
|
this.routeSub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
src/app/process-page/new/new-process.component.html
Normal file
6
src/app/process-page/new/new-process.component.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<ng-container *ngIf="fromExisting$ && (fromExisting$ | async)">
|
||||||
|
<ds-process-form *ngVar="fromExisting$ | async as process" headerKey="process.new.header" [selectedScript]="script$ | async" [parameters]="process.parameters"></ds-process-form>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!fromExisting$ || !(fromExisting$ | async)">
|
||||||
|
<ds-process-form headerKey="process.new.header"></ds-process-form>
|
||||||
|
</ng-container>
|
0
src/app/process-page/new/new-process.component.scss
Normal file
0
src/app/process-page/new/new-process.component.scss
Normal file
84
src/app/process-page/new/new-process.component.spec.ts
Normal file
84
src/app/process-page/new/new-process.component.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
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 { NewProcessComponent } from './new-process.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 { 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', () => {
|
||||||
|
let component: NewProcessComponent;
|
||||||
|
let fixture: ComponentFixture<NewProcessComponent>;
|
||||||
|
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: [NewProcessComponent, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
|
{ provide: RequestService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { snapshot: { queryParams: {} } } },
|
||||||
|
{ provide: LinkService, useValue: {} },
|
||||||
|
{ provide: ProcessDataService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NewProcessComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
47
src/app/process-page/new/new-process.component.ts
Normal file
47
src/app/process-page/new/new-process.component.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { LinkService } from '../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { Script } from '../scripts/script.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to create a new script
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-new-process',
|
||||||
|
templateUrl: './new-process.component.html',
|
||||||
|
styleUrls: ['./new-process.component.scss'],
|
||||||
|
})
|
||||||
|
export class NewProcessComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* Emits preselected process if there is one
|
||||||
|
*/
|
||||||
|
fromExisting$?: Observable<Process>;
|
||||||
|
/**
|
||||||
|
* Emits preselected script if there is one
|
||||||
|
*/
|
||||||
|
script$?: Observable<Script>;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute, private processService: ProcessDataService, private linkService: LinkService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there's an id parameter, use this the process with this identifier as presets for the form
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
const id = this.route.snapshot.queryParams.id;
|
||||||
|
if (id) {
|
||||||
|
this.fromExisting$ = this.processService.findById(id).pipe(getFirstSucceededRemoteDataPayload());
|
||||||
|
this.script$ = this.fromExisting$.pipe(
|
||||||
|
map((process: Process) => this.linkService.resolveLink<Process>(process, followLink('script'))),
|
||||||
|
switchMap((process: Process) => process.script),
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="d-flex">
|
||||||
|
<h2 class="flex-grow-1">{{'process.overview.title' | translate}}</h2>
|
||||||
|
<button class="btn btn-lg btn-success " routerLink="/processes/new"><i class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
<ds-pagination *ngIf="(processesRD$ | async)?.payload?.totalElements > 0"
|
||||||
|
[paginationOptions]="pageConfig"
|
||||||
|
[pageInfoState]="(processesRD$ | async)?.payload"
|
||||||
|
[collectionSize]="(processesRD$ | async)?.payload?.totalElements"
|
||||||
|
[hideGear]="true"
|
||||||
|
[hidePagerWhenSinglePage]="true"
|
||||||
|
(pageChange)="onPageChange($event)">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{{'process.overview.table.id' | translate}}</th>
|
||||||
|
<th scope="col">{{'process.overview.table.name' | translate}}</th>
|
||||||
|
<th scope="col">{{'process.overview.table.user' | translate}}</th>
|
||||||
|
<th scope="col">{{'process.overview.table.start' | translate}}</th>
|
||||||
|
<th scope="col">{{'process.overview.table.finish' | translate}}</th>
|
||||||
|
<th scope="col">{{'process.overview.table.status' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let process of (processesRD$ | async)?.payload?.page">
|
||||||
|
<td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td>
|
||||||
|
<td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td>
|
||||||
|
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
|
||||||
|
<td>{{process.startTime | date:dateFormat}}</td>
|
||||||
|
<td>{{process.endTime | date:dateFormat}}</td>
|
||||||
|
<td>{{process.processStatus}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</ds-pagination>
|
||||||
|
</div>
|
158
src/app/process-page/overview/process-overview.component.spec.ts
Normal file
158
src/app/process-page/overview/process-overview.component.spec.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { ProcessOverviewComponent } from './process-overview.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ProcessStatus } from '../processes/process-status.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
|
||||||
|
describe('ProcessOverviewComponent', () => {
|
||||||
|
let component: ProcessOverviewComponent;
|
||||||
|
let fixture: ComponentFixture<ProcessOverviewComponent>;
|
||||||
|
|
||||||
|
let processService: ProcessDataService;
|
||||||
|
let ePersonService: EPersonDataService;
|
||||||
|
|
||||||
|
let processes: Process[];
|
||||||
|
let ePerson: EPerson;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
processes = [
|
||||||
|
Object.assign(new Process(), {
|
||||||
|
processId: 1,
|
||||||
|
scriptName: 'script-name',
|
||||||
|
startTime: '2020-03-19',
|
||||||
|
endTime: '2020-03-19',
|
||||||
|
processStatus: ProcessStatus.COMPLETED
|
||||||
|
}),
|
||||||
|
Object.assign(new Process(), {
|
||||||
|
processId: 2,
|
||||||
|
scriptName: 'script-name',
|
||||||
|
startTime: '2020-03-20',
|
||||||
|
endTime: '2020-03-20',
|
||||||
|
processStatus: ProcessStatus.FAILED
|
||||||
|
}),
|
||||||
|
Object.assign(new Process(), {
|
||||||
|
processId: 3,
|
||||||
|
scriptName: 'another-script-name',
|
||||||
|
startTime: '2020-03-21',
|
||||||
|
endTime: '2020-03-21',
|
||||||
|
processStatus: ProcessStatus.RUNNING
|
||||||
|
})
|
||||||
|
];
|
||||||
|
ePerson = Object.assign(new EPerson(), {
|
||||||
|
metadata: {
|
||||||
|
'eperson.firstname': [
|
||||||
|
{
|
||||||
|
value: 'John',
|
||||||
|
language: null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'eperson.lastname': [
|
||||||
|
{
|
||||||
|
value: 'Doe',
|
||||||
|
language: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
processService = jasmine.createSpyObj('processService', {
|
||||||
|
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(processes))
|
||||||
|
});
|
||||||
|
ePersonService = jasmine.createSpyObj('ePersonService', {
|
||||||
|
findById: createSuccessfulRemoteDataObject$(ePerson)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProcessOverviewComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: ProcessDataService, useValue: processService },
|
||||||
|
{ provide: EPersonDataService, useValue: ePersonService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProcessOverviewComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('table structure', () => {
|
||||||
|
let rowElements;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rowElements = fixture.debugElement.queryAll(By.css('tbody tr'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should contain 3 rows`, () => {
|
||||||
|
expect(rowElements.length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the process IDs in the first column', () => {
|
||||||
|
rowElements.forEach((rowElement, index) => {
|
||||||
|
const el = rowElement.query(By.css('td:nth-child(1)')).nativeElement;
|
||||||
|
expect(el.textContent).toContain(processes[index].processId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the script names in the second column', () => {
|
||||||
|
rowElements.forEach((rowElement, index) => {
|
||||||
|
const el = rowElement.query(By.css('td:nth-child(2)')).nativeElement;
|
||||||
|
expect(el.textContent).toContain(processes[index].scriptName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the eperson\'s name in the third column', () => {
|
||||||
|
rowElements.forEach((rowElement, index) => {
|
||||||
|
const el = rowElement.query(By.css('td:nth-child(3)')).nativeElement;
|
||||||
|
expect(el.textContent).toContain(ePerson.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the start time in the fourth column', () => {
|
||||||
|
rowElements.forEach((rowElement, index) => {
|
||||||
|
const el = rowElement.query(By.css('td:nth-child(4)')).nativeElement;
|
||||||
|
expect(el.textContent).toContain(processes[index].startTime);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the end time in the fifth column', () => {
|
||||||
|
rowElements.forEach((rowElement, index) => {
|
||||||
|
const el = rowElement.query(By.css('td:nth-child(5)')).nativeElement;
|
||||||
|
expect(el.textContent).toContain(processes[index].endTime);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the status in the sixth column', () => {
|
||||||
|
rowElements.forEach((rowElement, index) => {
|
||||||
|
const el = rowElement.query(By.css('td:nth-child(6)')).nativeElement;
|
||||||
|
expect(el.textContent).toContain(processes[index].processStatus);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onPageChange', () => {
|
||||||
|
const toPage = 2;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component.onPageChange(toPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call a new findAll with the corresponding page', () => {
|
||||||
|
expect(processService.findAll).toHaveBeenCalledWith(jasmine.objectContaining({ currentPage: toPage }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
86
src/app/process-page/overview/process-overview.component.ts
Normal file
86
src/app/process-page/overview/process-overview.component.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { FindListOptions } from '../../core/data/request.models';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-process-overview',
|
||||||
|
templateUrl: './process-overview.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component displaying a list of all processes in a paginated table
|
||||||
|
*/
|
||||||
|
export class ProcessOverviewComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of all processes
|
||||||
|
*/
|
||||||
|
processesRD$: Observable<RemoteData<PaginatedList<Process>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current pagination configuration for the page used by the FindAll method
|
||||||
|
*/
|
||||||
|
config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
|
elementsPerPage: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current pagination configuration for the page
|
||||||
|
*/
|
||||||
|
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'process-overview-pagination',
|
||||||
|
pageSize: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date format to use for start and end time of processes
|
||||||
|
*/
|
||||||
|
dateFormat = 'yyyy-MM-dd HH:mm:ss';
|
||||||
|
|
||||||
|
constructor(protected processService: ProcessDataService,
|
||||||
|
protected ePersonService: EPersonDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.setProcesses();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the page is changed, make sure to update the list of processes to match the new page
|
||||||
|
* @param event The page change event
|
||||||
|
*/
|
||||||
|
onPageChange(event) {
|
||||||
|
this.config = Object.assign(new FindListOptions(), this.config, {
|
||||||
|
currentPage: event,
|
||||||
|
});
|
||||||
|
this.pageConfig.currentPage = event;
|
||||||
|
this.setProcesses();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a request to fetch all processes for the current page
|
||||||
|
*/
|
||||||
|
setProcesses() {
|
||||||
|
this.processesRD$ = this.processService.findAll(this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of an EPerson by ID
|
||||||
|
* @param id ID of the EPerson
|
||||||
|
*/
|
||||||
|
getEpersonName(id: string): Observable<string> {
|
||||||
|
return this.ePersonService.findById(id).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((eperson: EPerson) => eperson.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
src/app/process-page/process-breadcrumb.resolver.spec.ts
Normal file
40
src/app/process-page/process-breadcrumb.resolver.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { ProcessBreadcrumbResolver } from './process-breadcrumb.resolver';
|
||||||
|
import { Process } from './processes/process.model';
|
||||||
|
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
|
||||||
|
describe('ProcessBreadcrumbResolver', () => {
|
||||||
|
describe('resolve', () => {
|
||||||
|
let resolver: ProcessBreadcrumbResolver;
|
||||||
|
let processDataService: ProcessDataService;
|
||||||
|
let processBreadcrumbService: any;
|
||||||
|
let process: Process;
|
||||||
|
let id: string;
|
||||||
|
let path: string;
|
||||||
|
beforeEach(() => {
|
||||||
|
id = '12345';
|
||||||
|
process = Object.assign(new Process(), { id });
|
||||||
|
path = 'rest.com/path/to/breadcrumb/12345';
|
||||||
|
processBreadcrumbService = {};
|
||||||
|
processDataService = {
|
||||||
|
findById: () => createSuccessfulRemoteDataObject$(process)
|
||||||
|
} as any;
|
||||||
|
resolver = new ProcessBreadcrumbResolver(processBreadcrumbService, processDataService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the breadcrumb config', (done) => {
|
||||||
|
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: process }, params: { id: id} } as any, {url: path} as any);
|
||||||
|
const expectedConfig = { provider: processBreadcrumbService, key: process, url: path};
|
||||||
|
resolvedConfig.subscribe((config) => {
|
||||||
|
expect(config).toEqual(expectedConfig);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve throw an error when no breadcrumbKey is defined', () => {
|
||||||
|
expect(() => {
|
||||||
|
resolver.resolve({ data: {} } as any, undefined)
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
40
src/app/process-page/process-breadcrumb.resolver.ts
Normal file
40
src/app/process-page/process-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { Process } from './processes/process.model';
|
||||||
|
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||||
|
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
||||||
|
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { ProcessBreadcrumbsService } from './process-breadcrumbs.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific process before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ProcessBreadcrumbResolver implements Resolve<BreadcrumbConfig<Process>> {
|
||||||
|
constructor(protected breadcrumbService: ProcessBreadcrumbsService, private processService: ProcessDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a process based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns Observable<<RemoteData<Process>> Emits the found process based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<Process>> {
|
||||||
|
const id = route.params.id;
|
||||||
|
|
||||||
|
return this.processService.findById(route.params.id, followLink('script')).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((object: Process) => {
|
||||||
|
const fullPath = state.url;
|
||||||
|
const url = fullPath.substr(0, fullPath.indexOf(id)) + id;
|
||||||
|
return { provider: this.breadcrumbService, key: object, url: url };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
36
src/app/process-page/process-breadcrumbs.service.spec.ts
Normal file
36
src/app/process-page/process-breadcrumbs.service.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { ProcessBreadcrumbsService } from './process-breadcrumbs.service';
|
||||||
|
import { Breadcrumb } from '../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { Process } from './processes/process.model';
|
||||||
|
|
||||||
|
describe('ProcessBreadcrumbsService', () => {
|
||||||
|
let service: ProcessBreadcrumbsService;
|
||||||
|
let exampleId;
|
||||||
|
let exampleScriptName;
|
||||||
|
let exampleProcess;
|
||||||
|
let exampleURL;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
exampleId = '12345';
|
||||||
|
exampleScriptName = 'Example Script';
|
||||||
|
exampleProcess = Object.assign(new Process(), {processId: exampleId, scriptName: exampleScriptName});
|
||||||
|
exampleURL = 'example.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new ProcessBreadcrumbsService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBreadcrumbs', () => {
|
||||||
|
it('should return a breadcrumb based on a id and scriptName of the process', () => {
|
||||||
|
const breadcrumbs = service.getBreadcrumbs(exampleProcess, exampleURL);
|
||||||
|
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(exampleId + ' - ' + exampleScriptName, exampleURL)] });
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
21
src/app/process-page/process-breadcrumbs.service.ts
Normal file
21
src/app/process-page/process-breadcrumbs.service.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service';
|
||||||
|
import { Breadcrumb } from '../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { Process } from './processes/process.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to calculate process breadcrumbs for a single part of the route
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ProcessBreadcrumbsService implements BreadcrumbsService<Process> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to calculate the breadcrumbs
|
||||||
|
* @param key The key used to resolve the breadcrumb
|
||||||
|
* @param url The url to use as a link for this breadcrumb
|
||||||
|
*/
|
||||||
|
getBreadcrumbs(key: Process, url: string): Observable<Breadcrumb[]> {
|
||||||
|
return observableOf([new Breadcrumb(key.processId + ' - ' + key.scriptName, url)]);
|
||||||
|
}
|
||||||
|
}
|
50
src/app/process-page/process-page-routing.module.ts
Normal file
50
src/app/process-page/process-page-routing.module.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { NewProcessComponent } from './new/new-process.component';
|
||||||
|
import { ProcessOverviewComponent } from './overview/process-overview.component';
|
||||||
|
import { ProcessPageResolver } from './process-page.resolver';
|
||||||
|
import { ProcessDetailComponent } from './detail/process-detail.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { ProcessBreadcrumbResolver } from './process-breadcrumb.resolver';
|
||||||
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
data: { breadcrumbKey: 'process.overview' },
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: ProcessOverviewComponent,
|
||||||
|
data: { title: 'process.overview.title' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'new',
|
||||||
|
component: NewProcessComponent,
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
data: { title: 'process.new.title', breadcrumbKey: 'process.new' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: ProcessDetailComponent,
|
||||||
|
resolve: {
|
||||||
|
process: ProcessPageResolver,
|
||||||
|
breadcrumb: ProcessBreadcrumbResolver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ProcessPageResolver
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ProcessPageRoutingModule {
|
||||||
|
|
||||||
|
}
|
51
src/app/process-page/process-page.module.ts
Normal file
51
src/app/process-page/process-page.module.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { ProcessPageRoutingModule } from './process-page-routing.module';
|
||||||
|
import { NewProcessComponent } from './new/new-process.component';
|
||||||
|
import { ScriptsSelectComponent } from './form/scripts-select/scripts-select.component';
|
||||||
|
import { ScriptHelpComponent } from './form/script-help/script-help.component';
|
||||||
|
import { ParameterSelectComponent } from './form/process-parameters/parameter-select/parameter-select.component';
|
||||||
|
import { ProcessParametersComponent } from './form/process-parameters/process-parameters.component';
|
||||||
|
import { StringValueInputComponent } from './form/process-parameters/parameter-value-input/string-value-input/string-value-input.component';
|
||||||
|
import { ParameterValueInputComponent } from './form/process-parameters/parameter-value-input/parameter-value-input.component';
|
||||||
|
import { FileValueInputComponent } from './form/process-parameters/parameter-value-input/file-value-input/file-value-input.component';
|
||||||
|
import { BooleanValueInputComponent } from './form/process-parameters/parameter-value-input/boolean-value-input/boolean-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 { ProcessDetailComponent } from './detail/process-detail.component';
|
||||||
|
import { ProcessDetailFieldComponent } from './detail/process-detail-field/process-detail-field.component';
|
||||||
|
import { ProcessBreadcrumbsService } from './process-breadcrumbs.service';
|
||||||
|
import { ProcessBreadcrumbResolver } from './process-breadcrumb.resolver';
|
||||||
|
import { ProcessFormComponent } from './form/process-form.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
ProcessPageRoutingModule,
|
||||||
|
SharedModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
NewProcessComponent,
|
||||||
|
ScriptsSelectComponent,
|
||||||
|
ScriptHelpComponent,
|
||||||
|
ParameterSelectComponent,
|
||||||
|
ProcessParametersComponent,
|
||||||
|
StringValueInputComponent,
|
||||||
|
ParameterValueInputComponent,
|
||||||
|
FileValueInputComponent,
|
||||||
|
BooleanValueInputComponent,
|
||||||
|
DateValueInputComponent,
|
||||||
|
ProcessOverviewComponent,
|
||||||
|
ProcessDetailComponent,
|
||||||
|
ProcessDetailFieldComponent,
|
||||||
|
ProcessFormComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ProcessBreadcrumbResolver,
|
||||||
|
ProcessBreadcrumbsService
|
||||||
|
],
|
||||||
|
entryComponents: []
|
||||||
|
})
|
||||||
|
|
||||||
|
export class ProcessPageModule {
|
||||||
|
|
||||||
|
}
|
31
src/app/process-page/process-page.resolver.ts
Normal file
31
src/app/process-page/process-page.resolver.ts
Normal 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 { Process } from './processes/process.model';
|
||||||
|
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||||
|
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific process before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ProcessPageResolver implements Resolve<RemoteData<Process>> {
|
||||||
|
constructor(private processService: ProcessDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a process based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns Observable<<RemoteData<Process>> Emits the found process based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Process>> {
|
||||||
|
return this.processService.findById(route.params.id, followLink('script')).pipe(
|
||||||
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
src/app/process-page/processes/process-parameter.model.ts
Normal file
14
src/app/process-page/processes/process-parameter.model.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* A parameter used for running a process
|
||||||
|
*/
|
||||||
|
export class ProcessParameter {
|
||||||
|
/**
|
||||||
|
* The name of the parameter Eg. '-d', '-f' etc.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the parameter
|
||||||
|
*/
|
||||||
|
value: any;
|
||||||
|
}
|
9
src/app/process-page/processes/process-status.model.ts
Normal file
9
src/app/process-page/processes/process-status.model.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* List of process statuses
|
||||||
|
*/
|
||||||
|
export enum ProcessStatus {
|
||||||
|
SCHEDULED,
|
||||||
|
RUNNING,
|
||||||
|
COMPLETED,
|
||||||
|
FAILED
|
||||||
|
}
|
88
src/app/process-page/processes/process.model.ts
Normal file
88
src/app/process-page/processes/process.model.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { ProcessStatus } from './process-status.model';
|
||||||
|
import { ProcessParameter } from './process-parameter.model';
|
||||||
|
import { CacheableObject } from '../../core/cache/object-cache.reducer';
|
||||||
|
import { HALLink } from '../../core/shared/hal-link.model';
|
||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
import { PROCESS } from './process.resource-type';
|
||||||
|
import { excludeFromEquals } from '../../core/utilities/equals.decorators';
|
||||||
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
|
import { link, typedObject } from '../../core/cache/builders/build-decorators';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { SCRIPT } from '../scripts/script.resource-type';
|
||||||
|
import { Script } from '../scripts/script.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object representing a process
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class Process implements CacheableObject {
|
||||||
|
static type = PROCESS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier for this process
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
processId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UUID for the user that started the process
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The start time for this process
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
startTime: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The end time for this process
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
endTime: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the script run by this process
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
scriptName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status of this process
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
processStatus: ProcessStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parameters for this process
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
parameters: ProcessParameter[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Process
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
script: HALLink,
|
||||||
|
output: HALLink,
|
||||||
|
files: HALLink
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Script that created this Process
|
||||||
|
* Will be undefined unless the script {@link HALLink} has been resolved.
|
||||||
|
*/
|
||||||
|
@link(SCRIPT)
|
||||||
|
script?: Observable<RemoteData<Script>>;
|
||||||
|
}
|
9
src/app/process-page/processes/process.resource-type.ts
Normal file
9
src/app/process-page/processes/process.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* The resource type for Process
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
|
|
||||||
|
export const PROCESS = new ResourceType('process');
|
10
src/app/process-page/scripts/script-parameter-type.model.ts
Normal file
10
src/app/process-page/scripts/script-parameter-type.model.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* List of parameter types used for scripts
|
||||||
|
*/
|
||||||
|
export enum ScriptParameterType {
|
||||||
|
STRING = 'String',
|
||||||
|
DATE = 'date',
|
||||||
|
BOOLEAN = 'boolean',
|
||||||
|
FILE = 'InputStream',
|
||||||
|
OUTPUT = 'OutputStream'
|
||||||
|
}
|
31
src/app/process-page/scripts/script-parameter.model.ts
Normal file
31
src/app/process-page/scripts/script-parameter.model.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { ScriptParameterType } from './script-parameter-type.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A parameter that can be used when running a script
|
||||||
|
*/
|
||||||
|
export class ScriptParameter {
|
||||||
|
/**
|
||||||
|
* The name of the parameter Eg. '-d', '-f' etc.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short description about the purpose of this parameter
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of parameter
|
||||||
|
*/
|
||||||
|
type: ScriptParameterType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The long name for this parameter Eg. '--directory', '--force' etc.
|
||||||
|
*/
|
||||||
|
nameLong: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not this parameter is mandatory
|
||||||
|
*/
|
||||||
|
mandatory: boolean;
|
||||||
|
}
|
55
src/app/process-page/scripts/script.model.ts
Normal file
55
src/app/process-page/scripts/script.model.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { CacheableObject } from '../../core/cache/object-cache.reducer';
|
||||||
|
import { HALLink } from '../../core/shared/hal-link.model';
|
||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
import { SCRIPT } from './script.resource-type';
|
||||||
|
import { ScriptParameter } from './script-parameter.model';
|
||||||
|
import { typedObject } from '../../core/cache/builders/build-decorators';
|
||||||
|
import { excludeFromEquals } from '../../core/utilities/equals.decorators';
|
||||||
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object representing a script
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class Script implements CacheableObject {
|
||||||
|
static type = SCRIPT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of this script
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of this script
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A short description of this script
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available parameters for this script
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
parameters: ScriptParameter[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Script
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
};
|
||||||
|
}
|
9
src/app/process-page/scripts/script.resource-type.ts
Normal file
9
src/app/process-page/scripts/script.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* The resource type for Script
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
|
|
||||||
|
export const SCRIPT = new ResourceType('script');
|
@@ -62,10 +62,10 @@ export class EndpointMockingRestService extends DSpaceRESTv2Service {
|
|||||||
* @return Observable<DSpaceRESTV2Response>
|
* @return Observable<DSpaceRESTV2Response>
|
||||||
* An Observable<DSpaceRESTV2Response> containing the response from the server
|
* 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);
|
const mockData = this.getMockData(url);
|
||||||
if (isEmpty(mockData)) {
|
if (isEmpty(mockData)) {
|
||||||
return super.request(method, url, body, options);
|
return super.request(method, url, body, options, isMultipart);
|
||||||
} else {
|
} else {
|
||||||
return this.toMockResponse$(mockData);
|
return this.toMockResponse$(mockData);
|
||||||
}
|
}
|
||||||
|
@@ -189,6 +189,8 @@ import { CustomSwitchComponent } from './form/builder/ds-dynamic-form-ui/models/
|
|||||||
import { BundleListElementComponent } from './object-list/bundle-list-element/bundle-list-element.component';
|
import { BundleListElementComponent } from './object-list/bundle-list-element/bundle-list-element.component';
|
||||||
import { MissingTranslationHelper } from './translate/missing-translation.helper';
|
import { MissingTranslationHelper } from './translate/missing-translation.helper';
|
||||||
import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component';
|
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';
|
||||||
import { ModifyItemOverviewComponent } from '../+item-page/edit-item-page/modify-item-overview/modify-item-overview.component';
|
import { ModifyItemOverviewComponent } from '../+item-page/edit-item-page/modify-item-overview/modify-item-overview.component';
|
||||||
import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component';
|
import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component';
|
||||||
import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive';
|
import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive';
|
||||||
@@ -496,6 +498,9 @@ const DIRECTIVES = [
|
|||||||
MetadataRepresentationDirective,
|
MetadataRepresentationDirective,
|
||||||
ListableObjectDirective,
|
ListableObjectDirective,
|
||||||
ClaimedTaskActionsDirective,
|
ClaimedTaskActionsDirective,
|
||||||
|
FileValueAccessorDirective,
|
||||||
|
FileValidator,
|
||||||
|
ClaimedTaskActionsDirective,
|
||||||
NgForTrackByIdDirective
|
NgForTrackByIdDirective
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -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
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
27
src/app/shared/utils/file-value-accessor.directive.ts
Normal file
27
src/app/shared/utils/file-value-accessor.directive.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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 }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Value accessor directive for inputs of type 'file'
|
||||||
|
*/
|
||||||
|
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; }
|
||||||
|
}
|
22
src/app/shared/utils/require-file.validator.ts
Normal file
22
src/app/shared/utils/require-file.validator.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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 },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Validator directive to validate if a file is selected
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,6 @@
|
|||||||
import { Router } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router } from '@angular/router';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Util function to retrieve the current path (without query parameters) the user is on
|
* Util function to retrieve the current path (without query parameters) the user is on
|
||||||
@@ -8,3 +10,11 @@ export function currentPath(router: Router) {
|
|||||||
const urlTree = router.parseUrl(router.url);
|
const urlTree = router.parseUrl(router.url);
|
||||||
return '/' + urlTree.root.children.primary.segments.map((it) => it.path).join('/')
|
return '/' + urlTree.root.children.primary.segments.map((it) => it.path).join('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function currentPathFromSnapshot(route: ActivatedRouteSnapshot): string {
|
||||||
|
if (hasValue(route.parent)) {
|
||||||
|
const parentRoute: string = currentPathFromSnapshot(route.parent);
|
||||||
|
return new URLCombiner(parentRoute, route.routeConfig.path).toString();
|
||||||
|
}
|
||||||
|
return route.routeConfig ? route.routeConfig.path : '';
|
||||||
|
}
|
||||||
|
@@ -1733,6 +1733,8 @@
|
|||||||
|
|
||||||
"menu.section.icon.pin": "Pin sidebar",
|
"menu.section.icon.pin": "Pin sidebar",
|
||||||
|
|
||||||
|
"menu.section.icon.processes": "Processes menu section",
|
||||||
|
|
||||||
"menu.section.icon.registries": "Registries menu section",
|
"menu.section.icon.registries": "Registries menu section",
|
||||||
|
|
||||||
"menu.section.icon.statistics_task": "Statistics Task menu section",
|
"menu.section.icon.statistics_task": "Statistics Task menu section",
|
||||||
@@ -1759,6 +1761,8 @@
|
|||||||
|
|
||||||
"menu.section.new_item_version": "Item Version",
|
"menu.section.new_item_version": "Item Version",
|
||||||
|
|
||||||
|
"menu.section.new_process": "Process",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"menu.section.pin": "Pin sidebar",
|
"menu.section.pin": "Pin sidebar",
|
||||||
@@ -1767,6 +1771,10 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"menu.section.processes": "Processes",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"menu.section.registries": "Registries",
|
"menu.section.registries": "Registries",
|
||||||
|
|
||||||
"menu.section.registries_format": "Format",
|
"menu.section.registries_format": "Format",
|
||||||
@@ -1955,6 +1963,93 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"process.new.select-parameters": "Parameters",
|
||||||
|
|
||||||
|
"process.new.cancel": "Cancel",
|
||||||
|
|
||||||
|
"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",
|
||||||
|
|
||||||
|
"process.new.parameter.type.value": "value",
|
||||||
|
|
||||||
|
"process.new.parameter.type.file": "file",
|
||||||
|
|
||||||
|
"process.new.parameter.required.missing": "The following parameters are required but still missing:",
|
||||||
|
|
||||||
|
"process.new.notification.success.title": "Success",
|
||||||
|
|
||||||
|
"process.new.notification.success.content": "The process was successfully created",
|
||||||
|
|
||||||
|
"process.new.notification.error.title": "Error",
|
||||||
|
|
||||||
|
"process.new.notification.error.content": "An error occurred while creating this process",
|
||||||
|
|
||||||
|
"process.new.header": "Create a new process",
|
||||||
|
|
||||||
|
"process.new.title": "Create a new process",
|
||||||
|
|
||||||
|
"process.new.breadcrumbs": "Create a new process",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"process.detail.arguments" : "Arguments",
|
||||||
|
|
||||||
|
"process.detail.arguments.empty" : "This process doesn't contain any arguments",
|
||||||
|
|
||||||
|
"process.detail.back" : "Back",
|
||||||
|
|
||||||
|
"process.detail.output" : "Process Output",
|
||||||
|
|
||||||
|
"process.detail.output.alert" : "Work in progress - Process output is not available yet",
|
||||||
|
|
||||||
|
"process.detail.output-files" : "Output Files",
|
||||||
|
|
||||||
|
"process.detail.output-files.empty" : "This process doesn't contain any output files",
|
||||||
|
|
||||||
|
"process.detail.script" : "Script",
|
||||||
|
|
||||||
|
"process.detail.title" : "Process: {{ id }} - {{ name }}",
|
||||||
|
|
||||||
|
"process.detail.start-time" : "Start time",
|
||||||
|
|
||||||
|
"process.detail.end-time" : "Finish time",
|
||||||
|
|
||||||
|
"process.detail.status" : "Status",
|
||||||
|
|
||||||
|
"process.detail.create" : "Create similar process",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"process.overview.table.finish" : "Finish time",
|
||||||
|
|
||||||
|
"process.overview.table.id" : "Process ID",
|
||||||
|
|
||||||
|
"process.overview.table.name" : "Name",
|
||||||
|
|
||||||
|
"process.overview.table.start" : "Start time",
|
||||||
|
|
||||||
|
"process.overview.table.status" : "Status",
|
||||||
|
|
||||||
|
"process.overview.table.user" : "User",
|
||||||
|
|
||||||
|
"process.overview.title": "Processes Overview",
|
||||||
|
|
||||||
|
"process.overview.breadcrumbs": "Processes Overview",
|
||||||
|
|
||||||
|
"process.overview.new": "New",
|
||||||
|
|
||||||
|
|
||||||
"profile.breadcrumbs": "Update Profile",
|
"profile.breadcrumbs": "Update Profile",
|
||||||
|
|
||||||
"profile.card.identify": "Identify",
|
"profile.card.identify": "Identify",
|
||||||
|
@@ -50,3 +50,5 @@ if (hasValue(environment.universal) && environment.universal.preboot === false)
|
|||||||
} else {
|
} else {
|
||||||
document.addEventListener('DOMContentLoaded', () => bootloader(main));
|
document.addEventListener('DOMContentLoaded', () => bootloader(main));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import 'zone.js/dist/zone';
|
||||||
|
10
yarn.lock
10
yarn.lock
@@ -6813,10 +6813,12 @@ ng-mocks@^8.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-8.1.0.tgz#d00a5e53ae53587f35c68147826590fab71a1658"
|
resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-8.1.0.tgz#d00a5e53ae53587f35c68147826590fab71a1658"
|
||||||
integrity sha512-/314nyU6UrONCUKfvFRuJPLpNBxqocwJmQBlPy4he5Vueu6gObXjy+KLUlbbENuA7zTeBjp//RA6w/Fa1yQ4Fw==
|
integrity sha512-/314nyU6UrONCUKfvFRuJPLpNBxqocwJmQBlPy4he5Vueu6gObXjy+KLUlbbENuA7zTeBjp//RA6w/Fa1yQ4Fw==
|
||||||
|
|
||||||
ng2-file-upload@1.2.1:
|
ng2-file-upload@1.4.0:
|
||||||
version "1.2.1"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.2.1.tgz#5563c5dfd6f43fbfbe815c206e343464a0a6a197"
|
resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.4.0.tgz#8dea28d573234c52af474ad2a4001b335271e5c4"
|
||||||
integrity sha1-VWPF39b0P7++gVwgbjQ0ZKCmoZc=
|
integrity sha512-3J/KPU/tyh/ad6TFeUbrxX+SihUj0iOEt2Zsg4EX7mB3GFiQscXOfcUOxCkBtPWWWaqt3azrYbVGzsYa3/7NzQ==
|
||||||
|
dependencies:
|
||||||
|
tslib "^1.9.0"
|
||||||
|
|
||||||
ng2-nouislider@^1.8.2:
|
ng2-nouislider@^1.8.2:
|
||||||
version "1.8.2"
|
version "1.8.2"
|
||||||
|
Reference in New Issue
Block a user