From 2898cbac6ddd41a2749f1082c5814970e12caa8b Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Tue, 7 Jul 2020 18:46:50 +0200 Subject: [PATCH] 71713: Import metadata CSV --- .../metadata-import-page.component.html | 34 ++++ .../metadata-import-page.component.scss | 29 ++++ .../metadata-import-page.component.ts | 149 ++++++++++++++++++ src/app/+admin/admin-routing.module.ts | 7 + .../admin-sidebar/admin-sidebar.component.ts | 104 ++++++++---- src/app/+admin/admin.module.ts | 9 +- .../data/processes/script-data.service.ts | 9 +- ...export-metadata-selector.component.spec.ts | 4 +- .../export-metadata-selector.component.ts | 4 +- src/assets/i18n/en.json5 | 20 ++- 10 files changed, 322 insertions(+), 47 deletions(-) create mode 100644 src/app/+admin/admin-import-metadata-page/metadata-import-page.component.html create mode 100644 src/app/+admin/admin-import-metadata-page/metadata-import-page.component.scss create mode 100644 src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.html b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.html new file mode 100644 index 0000000000..2fc914d5f9 --- /dev/null +++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.html @@ -0,0 +1,34 @@ +
+ +

{{'admin.metadata-import.page.help' | translate}}

+ +
+
+
+
+

{{'admin.metadata-import.page.dropMsg' | translate}}

+
+
+
+ + +
+ + + +
diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.scss b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.scss new file mode 100644 index 0000000000..94b0fefb4b --- /dev/null +++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.scss @@ -0,0 +1,29 @@ +.ds-base-drop-zone { + border: 2px dashed $gray-600; +} + +.ds-document-drop-zone { + top: 0; + left: 0; + z-index: -1; +} + +.ds-document-drop-zone-active { + z-index: $drop-zone-area-z-index !important; +} + +.ds-document-drop-zone-inner { + background-color: rgba($white, 0.7); + z-index: $drop-zone-area-inner-z-index; + top: 0; + left: 0; +} + +.ds-document-drop-zone-inner-content { + border: 4px dashed map-get($theme-colors, primary); + z-index: $drop-zone-area-inner-z-index; +} + +.ds-document-drop-zone-inner-content p { + font-size: ($font-size-lg * 2.5); +} diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts new file mode 100644 index 0000000000..060ad42059 --- /dev/null +++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts @@ -0,0 +1,149 @@ +import { Location } from '@angular/common'; +import { Component, HostListener, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { uniqueId } from 'lodash'; +import { FileUploader } from 'ng2-file-upload'; +import { Observable } from 'rxjs/internal/Observable'; +import { filter, map, take, tap } from 'rxjs/operators'; +import { AuthService } from '../../core/auth/auth.service'; +import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service'; +import { RequestEntry } from '../../core/data/request.reducer'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { ProcessParameter } from '../../process-page/processes/process-parameter.model'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { of as observableOf } from 'rxjs'; +import { UploaderOptions } from '../../shared/uploader/uploader-options.model'; + +@Component({ + selector: 'ds-metadata-import-page', + templateUrl: './metadata-import-page.component.html', + styleUrls: ['./metadata-import-page.component.scss'] +}) + +/** + * Component that represents a metadata import page for administrators + */ +export class MetadataImportPageComponent implements OnInit { + + public isOverDocumentDropZone: Observable; + public uploader: FileUploader; + public uploaderId: string; + + /** + * The uploader configuration options + * @type {UploaderOptions} + */ + uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), { + // URL needs to contain something to not produce any errors. We are using onFileDrop; not the uploader + url: 'placeholder', + }); + + /** + * The current value of the file + */ + fileObject: File; + + /** + * The authenticated user's email + */ + private currentUserEmail$: Observable; + + public constructor(protected authService: AuthService, + private location: Location, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + private scriptDataService: ScriptDataService, + private router: Router) { + } + + /** + * Method provided by Angular. Invoked after the constructor. + */ + ngOnInit() { + this.currentUserEmail$ = this.authService.getAuthenticatedUserFromStore().pipe( + map((user: EPerson) => user.email) + ); + this.uploaderId = 'ds-drag-and-drop-uploader' + uniqueId(); + this.isOverDocumentDropZone = observableOf(false); + window.addEventListener('drop', e => { + e && e.preventDefault(); + }, false); + this.uploader = new FileUploader({ + // required, but using onFileDrop, not uploader + url: 'placeholder', + }); + } + + @HostListener('window:dragover', ['$event']) + onDragOver(event: any) { + // Show drop area on the page + event.preventDefault(); + if ((event.target as any).tagName !== 'HTML') { + this.isOverDocumentDropZone = observableOf(true); + } + } + + /** + * Called when files are dragged on the window document drop area. + */ + public fileOverDocument(isOver: boolean) { + if (!isOver) { + this.isOverDocumentDropZone = observableOf(isOver); + } + } + + /** + * Set (CSV) file + * @param files + */ + setFile(files) { + console.log('setfiles', files) + this.fileObject = files.length > 0 ? files[0] : undefined; + } + + /** + * When return button is pressed go to previous location + */ + public onReturn() { + this.location.back(); + } + + /** + * Starts import-metadata script with -e currentUserEmail -f fileName (and the selected file) + */ + public importMetadata() { + if (this.fileObject == null) { + this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); + } else { + this.currentUserEmail$.pipe( + filter((email: string) => hasValue(email)), + take(1) + ).subscribe((email: string) => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '-e', value: email }), + Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }), + ]; + this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]) + .pipe(take(1)) + .subscribe((requestEntry: RequestEntry) => { + if (requestEntry.response.isSuccessful) { + const title = this.translate.get('process.new.notification.success.title'); + const content = this.translate.get('process.new.notification.success.content'); + this.notificationsService.success(title, content); + const response: any = requestEntry.response; + if (isNotEmpty(response.resourceSelfLinks)) { + const processNumber = response.resourceSelfLinks[0].split('/').pop(); + this.router.navigateByUrl('/processes/' + processNumber); + } + } else { + const title = this.translate.get('process.new.notification.error.title'); + const content = this.translate.get('process.new.notification.error.content'); + this.notificationsService.error(title, content); + } + }); + }); + } + } +} diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index 43b3a4ab34..84b418772a 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { getAdminModulePath } from '../app-routing.module'; +import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; @@ -48,6 +49,12 @@ export function getAccessControlModulePath() { component: AdminCurationTasksComponent, data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' } }, + { + path: 'metadata-import', + resolve: { breadcrumb: I18nBreadcrumbResolver }, + component: MetadataImportPageComponent, + data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' } + }, ]) ], providers: [ diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index c6853ebc0a..eefc10c16e 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -2,13 +2,13 @@ import { Component, Injector, OnInit } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { combineLatest as observableCombineLatest } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; -import { of } from 'rxjs/internal/observable/of'; -import { first, map, take, tap, filter } from 'rxjs/operators'; +import { first, map, take } from 'rxjs/operators'; import { AuthService } from '../../core/auth/auth.service'; -import { ProcessDataService } from '../../core/data/processes/process-data.service'; -import { ScriptDataService } from '../../core/data/processes/script-data.service'; -import { RemoteData } from '../../core/data/remote-data'; -import { Script } from '../../process-page/scripts/script.model'; +import { + METADATA_EXPORT_SCRIPT_NAME, + METADATA_IMPORT_SCRIPT_NAME, + ScriptDataService +} from '../../core/data/processes/script-data.service'; import { slideHorizontal, slideSidebar } from '../../shared/animations/slide'; import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; @@ -17,8 +17,7 @@ import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; import { - ExportMetadataSelectorComponent, - METADATA_EXPORT_SCRIPT_NAME + ExportMetadataSelectorComponent } from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state'; import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model'; @@ -86,6 +85,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { this.createMenu(); this.createSiteAdministratorMenuSections(); this.createExportMenuSections(); + this.createImportMenuSections(); super.ngOnInit(); this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth'); this.authService.isAuthenticated() @@ -236,39 +236,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { } as OnClickMenuItemModel, }, - /* Import */ + /* Curation tasks */ { - id: 'import', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.import' - } as TextMenuItemModel, - icon: 'sign-in-alt', - index: 2 - }, - { - id: 'import_metadata', - parentID: 'import', + id: 'curation_tasks', active: false, visible: true, model: { type: MenuItemType.LINK, - text: 'menu.section.import_metadata', - link: '' - } as LinkMenuItemModel, - }, - { - id: 'import_batch', - parentID: 'import', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.import_batch', + text: 'menu.section.curation_task', link: '' } as LinkMenuItemModel, + icon: 'filter', + index: 7 }, /* Statistics */ @@ -401,6 +380,63 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { }); } + /** + * Create menu sections dependent on whether or not the current user is a site administrator and on whether or not + * the import scripts exist and the current user is allowed to execute them + */ + createImportMenuSections() { + const menuList = [ + /* Import */ + { + id: 'import', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.import' + } as TextMenuItemModel, + icon: 'sign-in-alt', + index: 2 + }, + { + id: 'import_batch', + parentID: 'import', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.import_batch', + link: '' + } as LinkMenuItemModel, + } + ]; + menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { + shouldPersistOnRouteChange: true + }))); + + observableCombineLatest( + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME) + ).pipe( + // TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed + // filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists), + take(1) + ).subscribe(() => { + this.menuService.addSection(this.menuID, { + id: 'import_metadata', + parentID: 'import', + active: true, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.import_metadata', + link: '/admin/metadata-import' + } as LinkMenuItemModel, + shouldPersistOnRouteChange: true + }); + }); + } + /** * Create menu sections dependent on whether or not the current user is a site administrator */ diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts index 85749afe03..c350272c3b 100644 --- a/src/app/+admin/admin.module.ts +++ b/src/app/+admin/admin.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '../shared/shared.module'; import { AdminAccessControlModule } from './admin-access-control/admin-access-control.module'; +import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; import { AdminRegistriesModule } from './admin-registries/admin-registries.module'; import { AdminRoutingModule } from './admin-routing.module'; import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; @@ -40,7 +41,9 @@ import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curati WorkflowItemSearchResultAdminWorkflowListElementComponent, WorkflowItemSearchResultAdminWorkflowGridElementComponent, - WorkflowItemAdminWorkflowActionsComponent + WorkflowItemAdminWorkflowActionsComponent, + + MetadataImportPageComponent ], entryComponents: [ @@ -54,7 +57,9 @@ import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curati WorkflowItemSearchResultAdminWorkflowListElementComponent, WorkflowItemSearchResultAdminWorkflowGridElementComponent, - WorkflowItemAdminWorkflowActionsComponent + WorkflowItemAdminWorkflowActionsComponent, + + MetadataImportPageComponent ] }) export class AdminModule { diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts index 696df8b441..34fa24cfff 100644 --- a/src/app/core/data/processes/script-data.service.ts +++ b/src/app/core/data/processes/script-data.service.ts @@ -1,8 +1,4 @@ import { Injectable } from '@angular/core'; -import { - getSucceededRemoteData, - getFirstSucceededRemoteDataPayload -} from '../../shared/operators'; import { DataService } from '../data.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { Store } from '@ngrx/store'; @@ -14,7 +10,7 @@ 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, filter } from 'rxjs/operators'; +import { find, map, switchMap } from 'rxjs/operators'; import { URLCombiner } from '../../url-combiner/url-combiner'; import { RemoteData } from '../remote-data'; import { MultipartPostRequest, RestRequest } from '../request.models'; @@ -25,6 +21,9 @@ import { dataService } from '../../cache/builders/build-decorators'; import { SCRIPT } from '../../../process-page/scripts/script.resource-type'; import { hasValue } from '../../../shared/empty.util'; +export const METADATA_IMPORT_SCRIPT_NAME: string = 'metadata-import'; +export const METADATA_EXPORT_SCRIPT_NAME: string = 'metadata-export'; + @Injectable() @dataService(SCRIPT) export class ScriptDataService extends DataService