forked from hazza/dspace-angular
71713: Import metadata CSV
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<div class="container">
|
||||
<h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2>
|
||||
<p>{{'admin.metadata-import.page.help' | translate}}</p>
|
||||
|
||||
<div ng2FileDrop
|
||||
class="ds-document-drop-zone position-fixed h-100 w-100"
|
||||
[class.ds-document-drop-zone-active]="(isOverDocumentDropZone | async)"
|
||||
[uploader]="uploader"
|
||||
(onFileDrop)="setFile($event)"
|
||||
(fileOver)="fileOverDocument($event)">
|
||||
</div>
|
||||
<div *ngIf="(isOverDocumentDropZone | async)"
|
||||
class="ds-document-drop-zone-inner position-fixed h-100 w-100 p-2">
|
||||
<div
|
||||
class="ds-document-drop-zone-inner-content position-relative d-flex flex-column justify-content-center text-center h-100 w-100">
|
||||
<p class="text-primary">{{'admin.metadata-import.page.dropMsg' | translate}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="well ds-base-drop-zone mt-1 mb-3 text-muted">
|
||||
<label for="file-upload" 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" id="file-upload" class="form-control-file d-none"
|
||||
[ngModel]="fileObject" (ngModelChange)="setFile($event)"/>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-secondary"
|
||||
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||
<button class="btn btn-primary"
|
||||
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||
</div>
|
@@ -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);
|
||||
}
|
@@ -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<boolean>;
|
||||
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<string>;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -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: [
|
||||
|
@@ -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
|
||||
*/
|
||||
|
@@ -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 {
|
||||
|
@@ -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<Script> {
|
||||
|
@@ -5,7 +5,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
@@ -13,7 +13,7 @@ import { ProcessParameter } from '../../../../process-page/processes/process-par
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||
import { ExportMetadataSelectorComponent, METADATA_EXPORT_SCRIPT_NAME } from './export-metadata-selector.component';
|
||||
import { ExportMetadataSelectorComponent } from './export-metadata-selector.component';
|
||||
|
||||
describe('ExportMetadataSelectorComponent', () => {
|
||||
let component: ExportMetadataSelectorComponent;
|
||||
|
@@ -4,7 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { take, map } from 'rxjs/operators';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { RequestEntry } from '../../../../core/data/request.reducer';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
@@ -16,8 +16,6 @@ import { isNotEmpty } from '../../../empty.util';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
|
||||
|
||||
export const METADATA_EXPORT_SCRIPT_NAME: string = 'metadata-export';
|
||||
|
||||
/**
|
||||
* Component to wrap a list of existing dso's inside a modal
|
||||
* Used to choose a dso from to export metadata of
|
||||
|
@@ -442,6 +442,24 @@
|
||||
|
||||
|
||||
|
||||
"admin.metadata-import.breadcrumbs": "Import Metadata",
|
||||
|
||||
"admin.metadata-import.title": "Import Metadata",
|
||||
|
||||
"admin.metadata-import.page.header": "Import Metadata",
|
||||
|
||||
"admin.metadata-import.page.help": "You can drop or browse CSV files that contain batch metadata operations on files here",
|
||||
|
||||
"admin.metadata-import.page.dropMsg": "Drop a metadata CSV to upload",
|
||||
|
||||
"admin.metadata-import.page.button.return": "Return",
|
||||
|
||||
"admin.metadata-import.page.button.proceed": "Proceed",
|
||||
|
||||
"admin.metadata-import.page.error.addFile": "Select file first!",
|
||||
|
||||
|
||||
|
||||
|
||||
"auth.errors.invalid-user": "Invalid email address or password.",
|
||||
|
||||
@@ -3092,6 +3110,6 @@
|
||||
|
||||
"workflow-item.send-back.button.cancel": "Cancel",
|
||||
|
||||
"workflow-item.send-back.button.confirm": "Send back",
|
||||
"workflow-item.send-back.button.confirm": "Send back"
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user