diff --git a/package.json b/package.json index 8d3c936212..21a89400bf 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "moment": "^2.22.1", "morgan": "^1.9.1", "ng-mocks": "^8.1.0", - "ng2-file-upload": "1.2.1", + "ng2-file-upload": "1.4.0", "ng2-nouislider": "^1.8.2", "ngx-bootstrap": "^5.3.2", "ngx-infinite-scroll": "6.0.1", diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index 5d885b918b..97b791c067 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -145,11 +145,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { this.modalService.open(CreateItemParentSelectorComponent); } } as OnClickMenuItemModel, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.new_item', - // link: '/submit' - // } as LinkMenuItemModel, + }, + { + id: 'new_process', + parentID: 'new', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.new_process', + link: '/processes/new' + } as LinkMenuItemModel, }, { id: 'new_item_version', @@ -439,6 +445,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { icon: 'cogs', 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 */ { id: 'workflow', diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index aace169f6c..3bd2f64961 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -114,6 +114,7 @@ export function getDSOPath(dso: DSpaceObject): string { path: PROFILE_MODULE_PATH, 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 }, ], { diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts index a06abdc816..6b640a9db0 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts @@ -1,21 +1,38 @@ import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; +import { URLCombiner } from '../url-combiner/url-combiner'; describe('I18nBreadcrumbResolver', () => { describe('resolve', () => { let resolver: I18nBreadcrumbResolver; let i18nBreadcrumbService: any; let i18nKey: string; - let path: string; + let route: any; + let parentSegment; + let segment; + let expectedPath; beforeEach(() => { 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 = {}; resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService); }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path], pathFromRoot: [{ url: [path] }] } as any, {} as any); - const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path }; + const resolvedConfig = resolver.resolve(route, {} as any); + const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index a6298628c7..cce36f590a 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; 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 @@ -25,17 +26,7 @@ export class I18nBreadcrumbResolver implements Resolve> if (hasNoValue(key)) { 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 }; } - - /** - * 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('/'); - } } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 1fd44224a9..8c990ae0b1 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; - import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; 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 { Version } from './shared/version.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 { WorkflowAction } from './tasks/models/workflow-action-object.model'; import { Registration } from './shared/registration.model'; @@ -258,6 +262,9 @@ const PROVIDERS = [ LicenseDataService, ItemTypeDataService, WorkflowActionDataService, + ProcessDataService, + ScriptDataService, + ProcessFilesResponseParsingService, MetadataSchemaDataService, MetadataFieldDataService, TokenResponseParsingService, @@ -309,6 +316,8 @@ export const models = ItemType, ExternalSource, ExternalSourceEntry, + Script, + Process, Version, VersionHistory, WorkflowAction, diff --git a/src/app/core/data/process-files-response-parsing.service.ts b/src/app/core/data/process-files-response-parsing.service.ts new file mode 100644 index 0000000000..0fa7c66869 --- /dev/null +++ b/src/app/core/data/process-files-response-parsing.service.ts @@ -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); + } +} diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts new file mode 100644 index 0000000000..48c1d502cc --- /dev/null +++ b/src/app/core/data/processes/process-data.service.ts @@ -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 { + protected linkPath = 'processes'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + /** + * Get the endpoint for a process his files + * @param processId The ID of the process + */ + getFilesEndpoint(processId: string): Observable { + 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>> { + 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>) => response.payload) + ); + + return this.rdbService.toRemoteDataObservable(requestEntry$, payload$); + } +} diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts new file mode 100644 index 0000000000..6600444ea0 --- /dev/null +++ b/src/app/core/data/processes/script-data.service.ts @@ -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