Merge branch 'main-gh4s' into CST-6782-refactor

This commit is contained in:
Davide Negretti
2022-10-03 17:35:26 +02:00
72 changed files with 5813 additions and 329 deletions

View File

@@ -135,6 +135,9 @@ languages:
- code: lv
label: Latviešu
active: true
- code: hi
label: Hindi
active: true
- code: hu
label: Magyar
active: true

View File

@@ -238,7 +238,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
if (restResponse.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name}));
this.reset();
} else {
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
}

View File

@@ -5,11 +5,12 @@ import { TranslateService } from '@ngx-translate/core';
import {
BehaviorSubject,
combineLatest as observableCombineLatest,
EMPTY,
Observable,
of as observableOf,
Subscription
} from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { catchError, defaultIfEmpty, map, switchMap, tap } from 'rxjs/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
@@ -144,7 +145,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
}
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
switchMap((isSiteAdmin: boolean) => {
return observableCombineLatest(groups.page.map((group: Group) => {
return observableCombineLatest([...groups.page.map((group: Group) => {
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
return observableCombineLatest([
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
@@ -165,8 +166,10 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
}
)
);
} else {
return EMPTY;
}
})).pipe(map((dtos: GroupDtoModel[]) => {
})]).pipe(defaultIfEmpty([]), map((dtos: GroupDtoModel[]) => {
return buildPaginatedList(groups.pageInfo, dtos);
}));
})

View File

@@ -0,0 +1,35 @@
<div class="container">
<h2 id="header">{{'admin.batch-import.page.header' | translate}}</h2>
<p>{{'admin.batch-import.page.help' | translate}}</p>
<p *ngIf="dso">
selected collection: <b>{{getDspaceObjectName()}}</b>&nbsp;
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
</p>
<p>
<button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button>
</p>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
<label class="form-check-label" for="validateOnly">
{{'admin.metadata-import.page.validateOnly' | translate}}
</label>
</div>
<small id="validateOnlyHelpBlock" class="form-text text-muted">
{{'admin.batch-import.page.validateOnly.hint' | translate}}
</small>
</div>
<ds-file-dropzone-no-uploader
(onFileAdded)="setFile($event)"
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
</ds-file-dropzone-no-uploader>
<div class="space-children-mr">
<button class="btn btn-secondary" id="backButton"
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
<button class="btn btn-primary" id="proceedButton"
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
</div>
</div>

View File

@@ -0,0 +1,151 @@
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
import { BatchImportPageComponent } from './batch-import-page.component';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { FormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
import { FileValidator } from '../../shared/utils/require-file.validator';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import {
BATCH_IMPORT_SCRIPT_NAME,
ScriptDataService
} from '../../core/data/processes/script-data.service';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
describe('BatchImportPageComponent', () => {
let component: BatchImportPageComponent;
let fixture: ComponentFixture<BatchImportPageComponent>;
let notificationService: NotificationsServiceStub;
let scriptService: any;
let router;
let locationStub;
function init() {
notificationService = new NotificationsServiceStub();
scriptService = jasmine.createSpyObj('scriptService',
{
invoke: createSuccessfulRemoteDataObject$({ processId: '46' })
}
);
router = jasmine.createSpyObj('router', {
navigateByUrl: jasmine.createSpy('navigateByUrl')
});
locationStub = jasmine.createSpyObj('location', {
back: jasmine.createSpy('back')
});
}
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule({
imports: [
FormsModule,
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([])
],
declarations: [BatchImportPageComponent, FileValueAccessorDirective, FileValidator],
providers: [
{ provide: NotificationsService, useValue: notificationService },
{ provide: ScriptDataService, useValue: scriptService },
{ provide: Router, useValue: router },
{ provide: Location, useValue: locationStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BatchImportPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('if back button is pressed', () => {
beforeEach(fakeAsync(() => {
const proceed = fixture.debugElement.query(By.css('#backButton')).nativeElement;
proceed.click();
fixture.detectChanges();
}));
it('should do location.back', () => {
expect(locationStub.back).toHaveBeenCalled();
});
});
describe('if file is set', () => {
let fileMock: File;
beforeEach(() => {
fileMock = new File([''], 'filename.zip', { type: 'application/zip' });
component.setFile(fileMock);
});
describe('if proceed button is pressed without validate only', () => {
beforeEach(fakeAsync(() => {
component.validateOnly = false;
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
proceed.click();
fixture.detectChanges();
}));
it('metadata-import script is invoked with --zip fileName and the mockFile', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
];
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' }));
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
});
it('success notification is shown', () => {
expect(notificationService.success).toHaveBeenCalled();
});
it('redirected to process page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
});
});
describe('if proceed button is pressed with validate only', () => {
beforeEach(fakeAsync(() => {
component.validateOnly = true;
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
proceed.click();
fixture.detectChanges();
}));
it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
Object.assign(new ProcessParameter(), { name: '--add' }),
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
});
it('success notification is shown', () => {
expect(notificationService.success).toHaveBeenCalled();
});
it('redirected to process page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
});
});
describe('if proceed is pressed; but script invoke fails', () => {
beforeEach(fakeAsync(() => {
jasmine.getEnv().allowRespy(true);
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
proceed.click();
fixture.detectChanges();
}));
it('error notification is shown', () => {
expect(notificationService.error).toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,124 @@
import { Component } from '@angular/core';
import { Location } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { BATCH_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
import { Router } from '@angular/router';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { RemoteData } from '../../core/data/remote-data';
import { Process } from '../../process-page/processes/process.model';
import { isNotEmpty } from '../../shared/empty.util';
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
import {
ImportBatchSelectorComponent
} from '../../shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { take } from 'rxjs/operators';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@Component({
selector: 'ds-batch-import-page',
templateUrl: './batch-import-page.component.html'
})
export class BatchImportPageComponent {
/**
* The current value of the file
*/
fileObject: File;
/**
* The validate only flag
*/
validateOnly = true;
/**
* dso object for community or collection
*/
dso: DSpaceObject = null;
public constructor(private location: Location,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
private scriptDataService: ScriptDataService,
private router: Router,
private modalService: NgbModal,
private dsoNameService: DSONameService) {
}
/**
* Set file
* @param file
*/
setFile(file) {
this.fileObject = file;
}
/**
* When return button is pressed go to previous location
*/
public onReturn() {
this.location.back();
}
public selectCollection() {
const modalRef = this.modalService.open(ImportBatchSelectorComponent);
modalRef.componentInstance.response.pipe(take(1)).subscribe((dso) => {
this.dso = dso || null;
});
}
/**
* Starts import-metadata script with --zip fileName (and the selected file)
*/
public importMetadata() {
if (this.fileObject == null) {
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
} else {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }),
Object.assign(new ProcessParameter(), { name: '--add' })
];
if (this.dso) {
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid }));
}
if (this.validateOnly) {
parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true }));
}
this.scriptDataService.invoke(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
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);
if (isNotEmpty(rd.payload)) {
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
}
} 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);
}
});
}
}
/**
* return selected dspace object name
*/
getDspaceObjectName(): string {
if (this.dso) {
return this.dsoNameService.getName(this.dso);
}
return null;
}
/**
* remove selected dso object
*/
removeDspaceObject(): void {
this.dso = null;
}
}

View File

@@ -7,6 +7,7 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
@NgModule({
imports: [
@@ -40,6 +41,12 @@ import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
component: MetadataImportPageComponent,
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
},
{
path: 'batch-import',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: BatchImportPageComponent,
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
},
])
],
providers: [

View File

@@ -9,6 +9,7 @@ import { AdminWorkflowModuleModule } from './admin-workflow-page/admin-workflow.
import { AdminSearchModule } from './admin-search-page/admin-search.module';
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -28,7 +29,8 @@ const ENTRY_COMPONENTS = [
],
declarations: [
AdminCurationTasksComponent,
MetadataImportPageComponent
MetadataImportPageComponent,
BatchImportPageComponent
]
})
export class AdminModule {

View File

@@ -25,6 +25,7 @@ import { ObjectCacheService } from '../../cache/object-cache.service';
import { HALDataService } from './hal-data-service.interface';
import { getFirstCompletedRemoteData } from '../../shared/operators';
export const EMBED_SEPARATOR = '%2F';
/**
* Common functionality for data services.
* Specific functionality that not all services would need
@@ -202,7 +203,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
let nestEmbed = embedString;
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) {
nestEmbed = nestEmbed + '/' + String(linkToFollow.name);
nestEmbed = nestEmbed + EMBED_SEPARATOR + String(linkToFollow.name);
// Add the nested embeds size if given in the FollowLinkConfig.FindListOptions
if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) {
const nestedEmbedSize = 'embed.size=' + nestEmbed.split('=')[1] + '=' + linkToFollow.findListOptions.elementsPerPage;

View File

@@ -22,6 +22,7 @@ import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.s
import { HALEndpointService } from '../../shared/hal-endpoint.service';
import { ObjectCacheService } from '../../cache/object-cache.service';
import { Observable, of as observableOf } from 'rxjs';
import { EMBED_SEPARATOR } from './base-data.service';
/**
* Tests whether calls to `FindAllData` methods are correctly patched through in a concrete data service that implements it
@@ -276,7 +277,7 @@ describe('FindAllDataImpl', () => {
});
it('should include nested linksToFollow 3lvl', () => {
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
const expected = `${endpoint}?embed=owningCollection${EMBED_SEPARATOR}itemtemplate${EMBED_SEPARATOR}relationships`;
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships')))).subscribe((value) => {
expect(value).toBe(expected);
@@ -284,7 +285,7 @@ describe('FindAllDataImpl', () => {
});
it('should include nested linksToFollow 2lvl and nested embed\'s size', () => {
const expected = `${endpoint}?embed.size=owningCollection/itemtemplate=4&embed=owningCollection/itemtemplate`;
const expected = `${endpoint}?embed.size=owningCollection${EMBED_SEPARATOR}itemtemplate=4&embed=owningCollection${EMBED_SEPARATOR}itemtemplate`;
const config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 4,
});

View File

@@ -19,6 +19,7 @@ import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.s
import { HALEndpointService } from '../../shared/hal-endpoint.service';
import { ObjectCacheService } from '../../cache/object-cache.service';
import { IdentifiableDataService } from './identifiable-data.service';
import { EMBED_SEPARATOR } from './base-data.service';
const endpoint = 'https://rest.api/core';
@@ -137,7 +138,7 @@ describe('IdentifiableDataService', () => {
});
it('should include nested linksToFollow 3lvl', () => {
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection${EMBED_SEPARATOR}itemtemplate${EMBED_SEPARATOR}relationships`;
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships'))));
expect(result).toEqual(expected);
});

View File

@@ -9,6 +9,7 @@ import { GetRequest, IdentifierType } from './request.models';
import { RequestService } from './request.service';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { Item } from '../shared/item.model';
import { EMBED_SEPARATOR } from './base/base-data.service';
describe('DsoRedirectService', () => {
let scheduler: TestScheduler;
@@ -174,7 +175,7 @@ describe('DsoRedirectService', () => {
});
it('should include nested linksToFollow 3lvl', () => {
const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`;
const expected = `${requestUUIDURL}&embed=owningCollection${EMBED_SEPARATOR}itemtemplate${EMBED_SEPARATOR}relationships`;
const result = (service as any).dataService.getIDHref(
pidLink,
dsoUUID,

View File

@@ -24,6 +24,8 @@ import { dataService } from '../base/data-service.decorator';
export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import';
export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export';
export const BATCH_IMPORT_SCRIPT_NAME = 'import';
export const BATCH_EXPORT_SCRIPT_NAME = 'export';
@Injectable()
@dataService(SCRIPT)

View File

@@ -6,6 +6,10 @@
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="lead item-list-title dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail>
</span>
</div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>

View File

@@ -6,6 +6,10 @@
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="lead item-list-title dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail>
</span>
</div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>

View File

@@ -5,6 +5,10 @@
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="lead item-list-title dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail>
</span>
</div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>

View File

@@ -1,33 +1,40 @@
<div class="row">
<div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
[alt]="'thumbnail.orgunit.alt'"
[placeholder]="'thumbnail.orgunit.placeholder'">
</ds-thumbnail>
</a>
</div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead"
[innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead"
[innerHTML]="dsoTitle"></span>
<span class="text-muted">
<div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
[alt]="'thumbnail.orgunit.alt'"
[placeholder]="'thumbnail.orgunit.placeholder'">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
[alt]="'thumbnail.orgunit.alt'"
[placeholder]="'thumbnail.orgunit.placeholder'">
</ds-thumbnail>
</span>
</div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead"
[innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead"
[innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)"></span>
<span class="text-muted">
<span *ngIf="dso.allMetadata(['dc.description']).length > 0"
class="item-list-org-unit-description">
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
[innerHTML]="firstMetadataValue('dc.description')"></span>
[innerHTML]="firstMetadataValue('dc.description')"></span>
</ds-truncatable-part>
</span>
</span>
</ds-truncatable>
</div>
</ds-truncatable>
</div>
</div>

View File

@@ -10,6 +10,8 @@ import { ItemSearchResult } from '../../../../../shared/object-collection/shared
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
let orgUnitListElementComponent: OrgUnitSearchResultListElementComponent;
let fixture: ComponentFixture<OrgUnitSearchResultListElementComponent>;
@@ -66,6 +68,13 @@ const enviromentNoThumbs = {
describe('OrgUnitSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}
)],
declarations: [ OrgUnitSearchResultListElementComponent , TruncatePipe],
providers: [
{ provide: TruncatableService, useValue: {} },
@@ -129,6 +138,13 @@ describe('OrgUnitSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}
)],
declarations: [OrgUnitSearchResultListElementComponent, TruncatePipe],
providers: [
{provide: TruncatableService, useValue: {}},

View File

@@ -9,6 +9,13 @@
[placeholder]="'thumbnail.person.placeholder'">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/person-placeholder.svg'"
[alt]="'thumbnail.person.alt'"
[placeholder]="'thumbnail.person.placeholder'">
</ds-thumbnail>
</span>
</div>
<div [ngClass]="showThumbnails ? 'col-9 col-md-10' : 'col-12'">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
@@ -16,10 +23,10 @@
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead"
[innerHTML]="dsoTitle"></a>
[innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead"
[innerHTML]="dsoTitle"></span>
[innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)"></span>
<span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1">
<span *ngIf="dso.allMetadata(['person.jobTitle']).length > 0"

View File

@@ -10,6 +10,8 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
let personListElementComponent: PersonSearchResultListElementComponent;
let fixture: ComponentFixture<PersonSearchResultListElementComponent>;
@@ -66,6 +68,13 @@ const enviromentNoThumbs = {
describe('PersonSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}
)],
declarations: [PersonSearchResultListElementComponent, TruncatePipe],
providers: [
{ provide: TruncatableService, useValue: {} },
@@ -129,6 +138,13 @@ describe('PersonSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}
)],
declarations: [PersonSearchResultListElementComponent, TruncatePipe],
providers: [
{provide: TruncatableService, useValue: {}},

View File

@@ -9,6 +9,13 @@
[placeholder]="'thumbnail.project.placeholder'">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/project-placeholder.svg'"
[alt]="'thumbnail.project.alt'"
[placeholder]="'thumbnail.project.placeholder'">
</ds-thumbnail>
</span>
</div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<ds-truncatable [id]="dso.id">

View File

@@ -8,6 +8,13 @@
[placeholder]="'thumbnail.person.placeholder'">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/person-placeholder.svg'"
[alt]="'thumbnail.person.alt'"
[placeholder]="'thumbnail.person.placeholder'">
</ds-thumbnail>
</span>
</div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<div class="d-flex">

View File

@@ -6,6 +6,8 @@ import { By } from '@angular/platform-browser';
import { MetadataUriValuesComponent } from './metadata-uri-values.component';
import { isNotEmpty } from '../../../shared/empty.util';
import { MetadataValue } from '../../../core/shared/metadata.models';
import { APP_CONFIG } from '../../../../config/app-config.interface';
import { environment } from '../../../../environments/environment';
let comp: MetadataUriValuesComponent;
let fixture: ComponentFixture<MetadataUriValuesComponent>;
@@ -33,6 +35,9 @@ describe('MetadataUriValuesComponent', () => {
useClass: TranslateLoaderMock
}
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
],
declarations: [MetadataUriValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(MetadataUriValuesComponent, {

View File

@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock
import { MetadataValuesComponent } from './metadata-values.component';
import { By } from '@angular/platform-browser';
import { MetadataValue } from '../../../core/shared/metadata.models';
import { APP_CONFIG } from '../../../../config/app-config.interface';
import { environment } from '../../../../environments/environment';
let comp: MetadataValuesComponent;
let fixture: ComponentFixture<MetadataValuesComponent>;
@@ -32,8 +34,11 @@ describe('MetadataValuesComponent', () => {
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
},
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
],
declarations: [MetadataValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(MetadataValuesComponent, {

View File

@@ -1,6 +1,6 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Component, Inject, Input, OnChanges, SimpleChanges } from '@angular/core';
import { MetadataValue } from '../../../core/shared/metadata.models';
import { environment } from '../../../../environments/environment';
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
/**
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
@@ -13,6 +13,11 @@ import { environment } from '../../../../environments/environment';
})
export class MetadataValuesComponent implements OnChanges {
constructor(
@Inject(APP_CONFIG) private appConfig: AppConfig,
) {
}
/**
* The metadata values to display
*/
@@ -41,6 +46,6 @@ export class MetadataValuesComponent implements OnChanges {
renderMarkdown;
ngOnChanges(changes: SimpleChanges): void {
this.renderMarkdown = !!environment.markdown.enabled && this.enableMarkdown;
this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown;
}
}

View File

@@ -3,16 +3,14 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ItemPageAbstractFieldComponent } from './item-page-abstract-field.component';
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { SharedModule } from '../../../../../shared/shared.module';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
import { By } from '@angular/platform-browser';
let comp: ItemPageAbstractFieldComponent;
let fixture: ComponentFixture<ItemPageAbstractFieldComponent>;
const mockField = 'dc.description.abstract';
const mockValue = 'test value';
describe('ItemPageAbstractFieldComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
@@ -25,7 +23,10 @@ describe('ItemPageAbstractFieldComponent', () => {
}),
SharedModule,
],
declarations: [ItemPageAbstractFieldComponent, MetadataValuesComponent],
providers: [
{ provide: APP_CONFIG, useValue: environment },
],
declarations: [ItemPageAbstractFieldComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemPageAbstractFieldComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
@@ -33,13 +34,13 @@ describe('ItemPageAbstractFieldComponent', () => {
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(ItemPageAbstractFieldComponent);
comp = fixture.componentInstance;
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
fixture.detectChanges();
}));
it('should display display the correct metadata value', () => {
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
it('should render a ds-metadata-values', () => {
expect(fixture.debugElement.query(By.css('ds-metadata-values'))).toBeDefined();
});
});

View File

@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loa
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { ItemPageAuthorFieldComponent } from './item-page-author-field.component';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
let comp: ItemPageAuthorFieldComponent;
let fixture: ComponentFixture<ItemPageAuthorFieldComponent>;
@@ -21,6 +23,9 @@ describe('ItemPageAuthorFieldComponent', () => {
useClass: TranslateLoaderMock
}
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
],
declarations: [ItemPageAuthorFieldComponent, MetadataValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemPageAuthorFieldComponent, {

View File

@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loa
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { ItemPageDateFieldComponent } from './item-page-date-field.component';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
let comp: ItemPageDateFieldComponent;
let fixture: ComponentFixture<ItemPageDateFieldComponent>;
@@ -21,6 +23,9 @@ describe('ItemPageDateFieldComponent', () => {
useClass: TranslateLoaderMock
}
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
],
declarations: [ItemPageDateFieldComponent, MetadataValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemPageDateFieldComponent, {

View File

@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loa
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { GenericItemPageFieldComponent } from './generic-item-page-field.component';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
let comp: GenericItemPageFieldComponent;
let fixture: ComponentFixture<GenericItemPageFieldComponent>;
@@ -23,6 +25,9 @@ describe('GenericItemPageFieldComponent', () => {
useClass: TranslateLoaderMock
}
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
],
declarations: [GenericItemPageFieldComponent, MetadataValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(GenericItemPageFieldComponent, {

View File

@@ -23,8 +23,16 @@ const mockLabel = 'test label';
const mockFields = [mockField];
describe('ItemPageFieldComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
let appConfig = Object.assign({}, environment, {
markdown: {
enabled: false,
mathjax: false,
}
});
const buildTestEnvironment = async () => {
await TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
@@ -35,7 +43,7 @@ describe('ItemPageFieldComponent', () => {
SharedModule,
],
providers: [
{ provide: APP_CONFIG, useValue: Object.assign({}, environment) },
{ provide: APP_CONFIG, useValue: appConfig },
],
declarations: [ItemPageFieldComponent, MetadataValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
@@ -43,26 +51,25 @@ describe('ItemPageFieldComponent', () => {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
markdownSpy = spyOn(MarkdownPipe.prototype, 'transform');
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(ItemPageFieldComponent);
comp = fixture.componentInstance;
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
comp.fields = mockFields;
comp.label = mockLabel;
fixture.detectChanges();
}));
};
it('should display display the correct metadata value', () => {
it('should display display the correct metadata value', waitForAsync(async () => {
await buildTestEnvironment();
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
});
}));
describe('when markdown is disabled in the environment config', () => {
beforeEach(() => {
TestBed.inject(APP_CONFIG).markdown.enabled = false;
});
beforeEach(waitForAsync(async () => {
appConfig.markdown.enabled = false;
await buildTestEnvironment();
}));
describe('and markdown is disabled in this component', () => {
@@ -91,9 +98,10 @@ describe('ItemPageFieldComponent', () => {
describe('when markdown is enabled in the environment config', () => {
beforeEach(() => {
TestBed.inject(APP_CONFIG).markdown.enabled = true;
});
beforeEach(waitForAsync(async () => {
appConfig.markdown.enabled = true;
await buildTestEnvironment();
}));
describe('and markdown is disabled in this component', () => {

View File

@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loa
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { ItemPageUriFieldComponent } from './item-page-uri-field.component';
import { MetadataUriValuesComponent } from '../../../../field-components/metadata-uri-values/metadata-uri-values.component';
import { environment } from '../../../../../../environments/environment';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
let comp: ItemPageUriFieldComponent;
let fixture: ComponentFixture<ItemPageUriFieldComponent>;
@@ -22,6 +24,9 @@ describe('ItemPageUriFieldComponent', () => {
useClass: TranslateLoaderMock
}
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
],
declarations: [ItemPageUriFieldComponent, MetadataUriValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemPageUriFieldComponent, {

View File

@@ -259,9 +259,15 @@ describe('MenuResolver', () => {
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'import', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'import_batch', parentID: 'import', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'export', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
id: 'export_batch', parentID: 'export', visible: true,
}));
});
});

View File

@@ -44,6 +44,9 @@ import {
METADATA_IMPORT_SCRIPT_NAME,
ScriptDataService
} from './core/data/processes/script-data.service';
import {
ExportBatchSelectorComponent
} from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
/**
* Creates all of the app's menus
@@ -440,6 +443,20 @@ export class MenuResolver implements Resolve<boolean> {
} as OnClickMenuItemModel,
shouldPersistOnRouteChange: true
});
this.menuService.addSection(MenuID.ADMIN, {
id: 'export_batch',
parentID: 'export',
active: false,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.export_batch',
function: () => {
this.modalService.open(ExportBatchSelectorComponent);
}
} as OnClickMenuItemModel,
shouldPersistOnRouteChange: true
});
});
}
@@ -448,20 +465,7 @@ export class MenuResolver implements Resolve<boolean> {
* the import scripts exist and the current user is allowed to execute them
*/
createImportMenuSections() {
const menuList = [
// TODO: enable this menu item once the feature has been implemented
// {
// id: 'import_batch',
// parentID: 'import',
// active: false,
// visible: true,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.import_batch',
// link: ''
// } as LinkMenuItemModel,
// }
];
const menuList = [];
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection));
observableCombineLatest([
@@ -498,6 +502,18 @@ export class MenuResolver implements Resolve<boolean> {
} as LinkMenuItemModel,
shouldPersistOnRouteChange: true
});
this.menuService.addSection(MenuID.ADMIN, {
id: 'import_batch',
parentID: 'import',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.import_batch',
link: '/admin/batch-import'
} as LinkMenuItemModel,
shouldPersistOnRouteChange: true
});
});
}

View File

@@ -16,6 +16,7 @@ import { WorkflowItemSearchResultListElementComponent } from '../shared/object-l
import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component';
import { ClaimedApprovedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component';
import { ClaimedDeclinedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component';
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -38,6 +39,7 @@ const ENTRY_COMPONENTS = [
CommonModule,
SharedModule,
MyDspacePageRoutingModule,
ResearchEntitiesModule.withEntryComponents()
],
declarations: [
...ENTRY_COMPONENTS

View File

@@ -10,7 +10,9 @@ export enum SelectorActionType {
CREATE = 'create',
EDIT = 'edit',
EXPORT_METADATA = 'export-metadata',
SET_SCOPE = 'set-scope'
IMPORT_BATCH = 'import-batch',
SET_SCOPE = 'set-scope',
EXPORT_BATCH = 'export-batch'
}
/**

View File

@@ -0,0 +1,210 @@
import { of as observableOf } from 'rxjs';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { DebugElement, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { NgbActiveModal, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { BATCH_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { Collection } from '../../../../core/shared/collection.model';
import { Item } from '../../../../core/shared/item.model';
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component';
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
import { NotificationsService } from '../../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$
} from '../../../remote-data.utils';
import { ExportBatchSelectorComponent } from './export-batch-selector.component';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
// No way to add entryComponents yet to testbed; alternative implemented; source: https://stackoverflow.com/questions/41689468/how-to-shallow-test-a-component-with-an-entrycomponents
@NgModule({
imports: [NgbModalModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}),
],
exports: [],
declarations: [ConfirmationModalComponent],
providers: []
})
class ModelTestModule {
}
describe('ExportBatchSelectorComponent', () => {
let component: ExportBatchSelectorComponent;
let fixture: ComponentFixture<ExportBatchSelectorComponent>;
let debugElement: DebugElement;
let modalRef;
let router;
let notificationService: NotificationsServiceStub;
let scriptService;
let authorizationDataService;
const mockItem = Object.assign(new Item(), {
id: 'fake-id',
uuid: 'fake-id',
handle: 'fake/handle',
lastModified: '2018'
});
const mockCollection: Collection = Object.assign(new Collection(), {
id: 'test-collection-1-1',
uuid: 'test-collection-1-1',
name: 'test-collection-1',
metadata: {
'dc.identifier.uri': [
{
language: null,
value: 'fake/test-collection-1'
}
]
}
});
const itemRD = createSuccessfulRemoteDataObject(mockItem);
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
beforeEach(waitForAsync(() => {
notificationService = new NotificationsServiceStub();
router = jasmine.createSpyObj('router', {
navigateByUrl: jasmine.createSpy('navigateByUrl')
});
scriptService = jasmine.createSpyObj('scriptService',
{
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
}
);
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
isAuthorized: observableOf(true)
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
declarations: [ExportBatchSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{ provide: NotificationsService, useValue: notificationService },
{ provide: ScriptDataService, useValue: scriptService },
{ provide: AuthorizationDataService, useValue: authorizationDataService },
{
provide: ActivatedRoute,
useValue: {
root: {
snapshot: {
data: {
dso: itemRD,
},
},
}
},
},
{
provide: Router, useValue: router
}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ExportBatchSelectorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
const modalService = TestBed.inject(NgbModal);
modalRef = modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.response = observableOf(true);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('if item is selected', () => {
let scriptRequestSucceeded;
beforeEach((done) => {
component.navigate(mockItem).subscribe((succeeded: boolean) => {
scriptRequestSucceeded = succeeded;
done();
});
});
it('should not invoke batch-export script', () => {
expect(scriptService.invoke).not.toHaveBeenCalled();
});
});
describe('if collection is selected and is admin', () => {
let scriptRequestSucceeded;
beforeEach((done) => {
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
scriptRequestSucceeded = succeeded;
done();
});
});
it('should invoke the batch-export script with option --id uuid option', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--id', value: mockCollection.uuid }),
Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []);
});
it('success notification is shown', () => {
expect(scriptRequestSucceeded).toBeTrue();
expect(notificationService.success).toHaveBeenCalled();
});
it('redirected to process page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
});
});
describe('if collection is selected and is not admin', () => {
let scriptRequestSucceeded;
beforeEach((done) => {
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
scriptRequestSucceeded = succeeded;
done();
});
});
it('should invoke the Batch-export script with option --id uuid without option', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--id', value: mockCollection.uuid }),
Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' })
];
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []);
});
it('success notification is shown', () => {
expect(scriptRequestSucceeded).toBeTrue();
expect(notificationService.success).toHaveBeenCalled();
});
it('redirected to process page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
});
});
describe('if collection is selected; but script invoke fails', () => {
let scriptRequestSucceeded;
beforeEach((done) => {
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
jasmine.getEnv().allowRespy(true);
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
scriptRequestSucceeded = succeeded;
done();
});
});
it('error notification is shown', () => {
expect(scriptRequestSucceeded).toBeFalse();
expect(notificationService.error).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,111 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { BATCH_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { Collection } from '../../../../core/shared/collection.model';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component';
import { isNotEmpty } from '../../../empty.util';
import { NotificationsService } from '../../../notifications/notifications.service';
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { Process } from '../../../../process-page/processes/process.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
/**
* Component to wrap a list of existing dso's inside a modal
* Used to choose a dso from to export metadata of
*/
@Component({
selector: 'ds-export-metadata-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class ExportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.DSPACEOBJECT;
selectorTypes = [DSpaceObjectType.COLLECTION];
action = SelectorActionType.EXPORT_BATCH;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
protected notificationsService: NotificationsService, protected translationService: TranslateService,
protected scriptDataService: ScriptDataService,
protected authorizationDataService: AuthorizationDataService,
private modalService: NgbModal) {
super(activeModal, route);
}
/**
* If the dso is a collection or community: start export-metadata script & navigate to process if successful
* Otherwise show error message
*/
navigate(dso: DSpaceObject): Observable<boolean> {
if (dso instanceof Collection) {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = dso;
modalRef.componentInstance.headerLabel = 'confirmation-modal.export-batch.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.export-batch.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.export-batch.cancel';
modalRef.componentInstance.confirmLabel = 'confirmation-modal.export-batch.confirm';
modalRef.componentInstance.confirmIcon = 'fas fa-file-export';
const resp$ = modalRef.componentInstance.response.pipe(switchMap((confirm: boolean) => {
if (confirm) {
const startScriptSucceeded$ = this.startScriptNotifyAndRedirect(dso);
return startScriptSucceeded$.pipe(
switchMap((r: boolean) => {
return observableOf(r);
})
);
} else {
const modalRefExport = this.modalService.open(ExportBatchSelectorComponent);
modalRefExport.componentInstance.dsoRD = createSuccessfulRemoteDataObject(dso);
}
}));
resp$.subscribe();
return resp$;
} else {
return observableOf(false);
}
}
/**
* Start export-metadata script of dso & navigate to process if successful
* Otherwise show error message
* @param dso Dso to export
*/
private startScriptNotifyAndRedirect(dso: DSpaceObject): Observable<boolean> {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--id', value: dso.uuid }),
Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' })
];
return this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf).pipe(
switchMap(() => {
return this.scriptDataService.invoke(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []);
}),
getFirstCompletedRemoteData(),
map((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
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);
if (isNotEmpty(rd.payload)) {
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
}
return true;
} 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);
return false;
}
})
);
}
}

View File

@@ -0,0 +1,77 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Collection } from '../../../../core/shared/collection.model';
import { Item } from '../../../../core/shared/item.model';
import { ImportBatchSelectorComponent } from './import-batch-selector.component';
describe('ImportBatchSelectorComponent', () => {
let component: ImportBatchSelectorComponent;
let fixture: ComponentFixture<ImportBatchSelectorComponent>;
const mockItem = Object.assign(new Item(), {
id: 'fake-id',
uuid: 'fake-id',
handle: 'fake/handle',
lastModified: '2018'
});
const mockCollection: Collection = Object.assign(new Collection(), {
id: 'test-collection-1-1',
uuid: 'test-collection-1-1',
name: 'test-collection-1',
metadata: {
'dc.identifier.uri': [
{
language: null,
value: 'fake/test-collection-1'
}
]
}
});
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
declarations: [ImportBatchSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ImportBatchSelectorComponent);
component = fixture.componentInstance;
spyOn(component.response, 'emit');
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('if item is selected', () => {
beforeEach((done) => {
component.navigate(mockItem).subscribe(() => {
done();
});
});
it('should emit null value', () => {
expect(component.response.emit).toHaveBeenCalledWith(null);
});
});
describe('if collection is selected', () => {
beforeEach((done) => {
component.navigate(mockCollection).subscribe(() => {
done();
});
});
it('should emit collection value', () => {
expect(component.response.emit).toHaveBeenCalledWith(mockCollection);
});
});
});

View File

@@ -0,0 +1,44 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Collection } from '../../../../core/shared/collection.model';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
import { Observable, of } from 'rxjs';
/**
* Component to wrap a list of existing dso's inside a modal
* Used to choose a dso from to import metadata of
*/
@Component({
selector: 'ds-import-batch-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class ImportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
objectType = DSpaceObjectType.DSPACEOBJECT;
selectorTypes = [DSpaceObjectType.COLLECTION];
action = SelectorActionType.IMPORT_BATCH;
/**
* An event fired when the modal is closed
*/
@Output()
response = new EventEmitter<DSpaceObject>();
constructor(protected activeModal: NgbActiveModal,
protected route: ActivatedRoute) {
super(activeModal, route);
}
/**
* If the dso is a collection:
*/
navigate(dso: DSpaceObject): Observable<null> {
if (dso instanceof Collection) {
this.response.emit(dso);
return of(null);
}
this.response.emit(null);
return of(null);
}
}

View File

@@ -1,15 +1,19 @@
<ng-container *ngIf="item$ | async">
<ds-themed-item-list-preview
[item]="item$ | async"
[object]="object"
[status]="status"></ds-themed-item-list-preview>
<div class="row">
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
<ds-workflowitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workflowitem-actions>
<ng-container *ngIf="derivedSearchResult$ | async">
<div class="row">
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
<ds-mydspace-item-status [status]="status"></ds-mydspace-item-status>
</div>
</div>
<ds-listable-object-component-loader
[viewMode]="ViewModes.ListElement"
[object]="derivedSearchResult$ | async" [linkType]="LinkTypes.None"></ds-listable-object-component-loader>
<div class="row">
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
<ds-workflowitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workflowitem-actions>
</div>
</div>
</div>
</ng-container>
<ds-themed-loading
*ngIf="!(item$ | async)"
[showMessage]="false"></ds-themed-loading>
*ngIf="!(derivedSearchResult$ | async)"
[showMessage]="false"></ds-themed-loading>

View File

@@ -92,14 +92,15 @@ describe('WorkflowItemSearchResultListElementComponent', () => {
}));
beforeEach(() => {
component.dso = mockResultObject.indexableObject;
component.object = mockResultObject;
fixture.detectChanges();
});
it('should init item properly', (done) => {
component.item$.pipe(take(1)).subscribe((i) => {
it('should init derivedSearchResult$ properly', (done) => {
component.derivedSearchResult$.pipe(take(1)).subscribe((i) => {
expect(linkService.resolveLink).toHaveBeenCalled();
expect(i).toBe(item);
expect(i.indexableObject).toBe(item);
expect(i.hitHighlights).toBe(mockResultObject.hitHighlights);
done();
});
});

View File

@@ -1,14 +1,12 @@
import { Component, Inject } from '@angular/core';
import { Observable } from 'rxjs';
import { find, map } from 'rxjs/operators';
import { map } from 'rxjs/operators';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { Item } from '../../../../core/shared/item.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
import { isNotUndefined } from '../../../empty.util';
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model';
@@ -17,6 +15,9 @@ import { followLink } from '../../../utils/follow-link-config.model';
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
import { CollectionElementLinkType } from '../../../object-collection/collection-element-link.type';
/**
* This component renders workflowitem object for the search result in the list view.
@@ -29,11 +30,14 @@ import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interfac
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement)
export class WorkflowItemSearchResultListElementComponent extends SearchResultListElementComponent<WorkflowItemSearchResult, WorkflowItem> {
LinkTypes = CollectionElementLinkType;
ViewModes = ViewMode;
/**
* The item object that belonging to the result object
* The item search result derived from the WorkspaceItemSearchResult
*/
public item$: Observable<Item>;
derivedSearchResult$: Observable<ItemSearchResult>;
/**
* Represent item's status
@@ -59,19 +63,19 @@ export class WorkflowItemSearchResultListElementComponent extends SearchResultLi
*/
ngOnInit() {
super.ngOnInit();
this.linkService.resolveLink(this.dso, followLink('item'));
this.initItem(this.dso.item as Observable<RemoteData<Item>> );
this.deriveSearchResult();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
/**
* Retrieve item from result object
*/
initItem(item$: Observable<RemoteData<Item>>) {
this.item$ = item$.pipe(
find((rd: RemoteData<Item>) => rd.hasSucceeded && isNotUndefined(rd.payload)),
map((rd: RemoteData<Item>) => rd.payload)
);
private deriveSearchResult() {
this.linkService.resolveLink(this.object.indexableObject, followLink('item'));
this.derivedSearchResult$ = this.object.indexableObject.item.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => {
const result = new ItemSearchResult();
result.indexableObject = item;
result.hitHighlights = this.object.hitHighlights;
return result;
}));
}
}

View File

@@ -1,15 +1,18 @@
<ng-container *ngIf="item$ | async">
<ds-themed-item-list-preview
[item]="item$ | async"
[object]="object"
[status]="status"></ds-themed-item-list-preview>
<div class="row">
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
<ds-workspaceitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workspaceitem-actions>
<ng-container *ngIf="derivedSearchResult$ | async">
<div class="row">
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
<ds-mydspace-item-status [status]="status"></ds-mydspace-item-status>
</div>
</div>
<ds-listable-object-component-loader
[viewMode]="ViewModes.ListElement"
[object]="derivedSearchResult$ | async" [linkType]="LinkTypes.None"></ds-listable-object-component-loader>
<div class="row">
<div [ngClass]="showThumbnails ? 'offset-3 offset-md-2 pl-3' : ''">
<ds-workspaceitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workspaceitem-actions>
</div>
</div>
</div>
</ng-container>
<ds-themed-loading
*ngIf="!(item$ | async)"
[showMessage]="false"></ds-themed-loading>
*ngIf="!(derivedSearchResult$ | async)"
[showMessage]="false"></ds-themed-loading>

View File

@@ -91,14 +91,15 @@ describe('WorkspaceItemSearchResultListElementComponent', () => {
}));
beforeEach(() => {
component.dso = mockResultObject.indexableObject;
component.object = mockResultObject;
fixture.detectChanges();
});
it('should init item properly', (done) => {
component.item$.pipe(take(1)).subscribe((i) => {
it('should init derivedSearchResult$ properly', (done) => {
component.derivedSearchResult$.pipe(take(1)).subscribe((i) => {
expect(linkService.resolveLink).toHaveBeenCalled();
expect(i).toBe(item);
expect(i.indexableObject).toBe(item);
expect(i.hitHighlights).toBe(mockResultObject.hitHighlights);
done();
});
});

View File

@@ -1,22 +1,23 @@
import { Component, Inject } from '@angular/core';
import { Observable } from 'rxjs';
import { find, map } from 'rxjs/operators';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { Item } from '../../../../core/shared/item.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model';
import { isNotUndefined } from '../../../empty.util';
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
import { WorkspaceItemSearchResult } from '../../../object-collection/shared/workspace-item-search-result.model';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { followLink } from '../../../utils/follow-link-config.model';
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
import { map } from 'rxjs/operators';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { CollectionElementLinkType } from '../../../object-collection/collection-element-link.type';
import { followLink } from '../../../utils/follow-link-config.model';
/**
* This component renders workspaceitem object for the search result in the list view.
@@ -28,12 +29,15 @@ import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interfac
})
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.ListElement)
export class WorkspaceItemSearchResultListElementComponent extends SearchResultListElementComponent<WorkspaceItemSearchResult, WorkspaceItem> {
export class WorkspaceItemSearchResultListElementComponent extends SearchResultListElementComponent<WorkspaceItemSearchResult, WorkspaceItem> {
LinkTypes = CollectionElementLinkType;
ViewModes = ViewMode;
/**
* The item object that belonging to the result object
* The item search result derived from the WorkspaceItemSearchResult
*/
item$: Observable<Item>;
derivedSearchResult$: Observable<ItemSearchResult>;
/**
* Represent item's status
@@ -59,18 +63,19 @@ export class WorkspaceItemSearchResultListElementComponent extends SearchResultL
*/
ngOnInit() {
super.ngOnInit();
this.linkService.resolveLink(this.dso, followLink('item'));
this.initItem(this.dso.item as Observable<RemoteData<Item>>);
this.deriveSearchResult();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
/**
* Retrieve item from result object
*/
initItem(item$: Observable<RemoteData<Item>>) {
this.item$ = item$.pipe(
find((rd: RemoteData<Item>) => rd.hasSucceeded && isNotUndefined(rd.payload)),
map((rd: RemoteData<Item>) => rd.payload)
);
private deriveSearchResult() {
this.linkService.resolveLink(this.object.indexableObject, followLink('item'));
this.derivedSearchResult$ = this.object.indexableObject.item.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => {
const result = new ItemSearchResult();
result.indexableObject = item;
result.hitHighlights = this.object.hitHighlights;
return result;
}));
}
}

View File

@@ -5,6 +5,10 @@
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail>
</span>
</div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<div class="d-flex">

View File

@@ -32,6 +32,7 @@ import { SelectionConfig } from './search-results/search-results.component';
import { ListableObject } from '../object-collection/shared/listable-object.model';
import { CollectionElementLinkType } from '../object-collection/collection-element-link.type';
import { environment } from 'src/environments/environment';
import { SubmissionObject } from '../../core/submission/models/submission-object.model';
@Component({
selector: 'ds-search',
@@ -362,6 +363,7 @@ export class SearchComponent implements OnInit {
this.useCachedVersionIfAvailable,
true,
followLink<Item>('thumbnail', { isOptional: true }),
followLink<SubmissionObject>('item', { isOptional: true }, followLink<Item>('thumbnail', { isOptional: true })) as any,
followLink<Item>('accessStatus', { isOptional: true, shouldEmbed: environment.item.showAccessStatuses })
).pipe(getFirstCompletedRemoteData())
.subscribe((results: RemoteData<SearchObjects<DSpaceObject>>) => {

View File

@@ -24,6 +24,12 @@ import { ConfirmationModalComponent } from './confirmation-modal/confirmation-mo
import {
ExportMetadataSelectorComponent
} from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
import {
ExportBatchSelectorComponent
} from './dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
import {
ImportBatchSelectorComponent
} from './dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component';
import { ItemListElementComponent } from './object-list/item-list-element/item-types/item/item-list-element.component';
import { EnumKeysPipe } from './utils/enum-keys-pipe';
@@ -478,6 +484,8 @@ const COMPONENTS = [
CollectionDropdownComponent,
EntityDropdownComponent,
ExportMetadataSelectorComponent,
ImportBatchSelectorComponent,
ExportBatchSelectorComponent,
ConfirmationModalComponent,
VocabularyTreeviewComponent,
AuthorizedCollectionSelectorComponent,
@@ -557,6 +565,8 @@ const ENTRY_COMPONENTS = [
BitstreamRequestACopyPageComponent,
CurationFormComponent,
ExportMetadataSelectorComponent,
ImportBatchSelectorComponent,
ExportBatchSelectorComponent,
ConfirmationModalComponent,
VocabularyTreeviewComponent,
SidebarSearchListElementComponent,

View File

@@ -40,7 +40,7 @@
</button>
</div>
<span *ngIf="uploader.progress < 100 && !(uploader.progress === 0 && !uploader.options.autoUpload)" class="float-right mr-3">{{ uploader.progress }}%</span>
<span *ngIf="uploader.progress === 100" class="float-right mr-3">{{'uploader.processing' | translate}}...</span>
<span *ngIf="uploader.progress === 100" class="float-right mr-3">{{'uploader.processing' | translate}}</span>
</div>
<div class="ds-base-drop-zone-progress clearfix mt-2">
<div role="progressbar"

View File

@@ -6651,9 +6651,9 @@
// TODO New key - Add a translation
"uploader.or": ", or ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO New key - Add a translation
"uploader.processing": "Processing",
"uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// "uploader.queue-length": "Queue length",
// TODO New key - Add a translation

View File

@@ -6026,7 +6026,8 @@
// "uploader.or": ", or ",
"uploader.or": "অথবা",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "প্রক্রিয়াকরণ",
// "uploader.queue-length": "Queue length",

View File

@@ -6518,9 +6518,9 @@
// TODO New key - Add a translation
"uploader.or": ", or ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO New key - Add a translation
"uploader.processing": "Processing",
"uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// "uploader.queue-length": "Queue length",
// TODO New key - Add a translation

View File

@@ -5381,7 +5381,8 @@
// "uploader.or": ", or ",
"uploader.or": ", oder",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "In Arbeit...",
// "uploader.queue-length": "Queue length",

View File

@@ -2203,6 +2203,8 @@
"uploader.delete.btn-title": "Διαγραφή",
"uploader.drag-message": "Σύρετε και αποθέστε τα αρχεία σας εδώ",
"uploader.or": ", ή",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Επεξεργασία",
"uploader.queue-length": "Μέγεθος ουράς",
"virtual-metadata.delete-item.info": "Επιλέξτε τους τύπους για τους οποίους θέλετε να αποθηκεύσετε τα εικονικά μεταδεδομένα ως πραγματικά μεταδεδομένα",

View File

@@ -542,28 +542,45 @@
"admin.metadata-import.breadcrumbs": "Import Metadata",
"admin.batch-import.breadcrumbs": "Import Batch",
"admin.metadata-import.title": "Import Metadata",
"admin.batch-import.title": "Import Batch",
"admin.metadata-import.page.header": "Import Metadata",
"admin.batch-import.page.header": "Import Batch",
"admin.metadata-import.page.help": "You can drop or browse CSV files that contain batch metadata operations on files here",
"admin.batch-import.page.help": "Select the Collection to import into. Then, drop or browse to a Simple Archive Format (SAF) zip file that includes the Items to import",
"admin.metadata-import.page.dropMsg": "Drop a metadata CSV to import",
"admin.batch-import.page.dropMsg": "Drop a batch ZIP to import",
"admin.metadata-import.page.dropMsgReplace": "Drop to replace the metadata CSV to import",
"admin.batch-import.page.dropMsgReplace": "Drop to replace the batch ZIP to import",
"admin.metadata-import.page.button.return": "Back",
"admin.metadata-import.page.button.proceed": "Proceed",
"admin.metadata-import.page.button.select-collection": "Select Collection",
"admin.metadata-import.page.error.addFile": "Select file first!",
"admin.batch-import.page.error.addFile": "Select Zip file first!",
"admin.metadata-import.page.validateOnly": "Validate Only",
"admin.metadata-import.page.validateOnly.hint": "When selected, the uploaded CSV will be validated. You will receive a report of detected changes, but no changes will be saved.",
"admin.batch-import.page.validateOnly.hint": "When selected, the uploaded ZIP will be validated. You will receive a report of detected changes, but no changes will be saved.",
"admin.batch-import.page.remove": "remove",
"auth.errors.invalid-user": "Invalid email address or password.",
@@ -1356,6 +1373,10 @@
"dso-selector.export-metadata.dspaceobject.head": "Export metadata from",
"dso-selector.export-batch.dspaceobject.head": "Export Batch (ZIP) from",
"dso-selector.import-batch.dspaceobject.head": "Import batch from",
"dso-selector.no-results": "No {{ type }} found",
"dso-selector.placeholder": "Search for a {{ type }}",
@@ -1384,6 +1405,14 @@
"confirmation-modal.export-metadata.confirm": "Export",
"confirmation-modal.export-batch.header": "Export batch (ZIP) for {{ dsoName }}",
"confirmation-modal.export-batch.info": "Are you sure you want to export batch (ZIP) for {{ dsoName }}",
"confirmation-modal.export-batch.cancel": "Cancel",
"confirmation-modal.export-batch.confirm": "Export",
"confirmation-modal.delete-eperson.header": "Delete EPerson \"{{ dsoName }}\"",
"confirmation-modal.delete-eperson.info": "Are you sure you want to delete EPerson \"{{ dsoName }}\"",
@@ -2627,6 +2656,7 @@
"menu.section.export_metadata": "Metadata",
"menu.section.export_batch": "Batch Export (ZIP)",
"menu.section.icon.access_control": "Access Control menu section",
@@ -2855,6 +2885,8 @@
"orgunit.listelement.badge": "Organizational Unit",
"orgunit.listelement.no-title": "Untitled",
"orgunit.page.city": "City",
"orgunit.page.country": "Country",
@@ -4463,7 +4495,7 @@
"uploader.or": ", or ",
"uploader.processing": "Processing",
"uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
"uploader.queue-length": "Queue length",

View File

@@ -6515,7 +6515,8 @@
// "uploader.or": ", or ",
"uploader.or": ", o ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Procesando",
// "uploader.queue-length": "Queue length",

View File

@@ -5061,7 +5061,8 @@
// "uploader.or": ", or ",
"uploader.or": " tai",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Käsitellään",
// "uploader.queue-length": "Queue length",

View File

@@ -1215,6 +1215,17 @@
// "collection.edit.tabs.authorizations.title": "Collection Edit - Authorizations",
"collection.edit.tabs.authorizations.title": "Édition de collection - Autorisations",
//"collection.edit.item.authorizations.load-bundle-button": "Load more bundles",
"collection.edit.item.authorizations.load-bundle-button": "Charger plus de Bundles",
//"collection.edit.item.authorizations.load-more-button": "Load more",
"collection.edit.item.authorizations.load-more-button": "Charger plus",
//"collection.edit.item.authorizations.show-bitstreams-button": "Show bitstream policies for bundle",
"collection.edit.item.authorizations.show-bitstreams-button": "Afficher les politiques de Bitstream pour le Bundle",
// "collection.edit.tabs.metadata.head": "Edit Metadata",
"collection.edit.tabs.metadata.head": "Éditer les Métadonnées",
@@ -2909,6 +2920,13 @@
// "item.search.title": "Item Search",
"item.search.title": "Recherche d'Items",
// "item.truncatable-part.show-more": "Show more",
"item.truncatable-part.show-more": "Voir plus",
//"item.truncatable-part.show-less": "Collapse",
"item.truncatable-part.show-less": "Réduire",
// "item.page.abstract": "Abstract",
"item.page.abstract": "Résumé",
@@ -3572,6 +3590,9 @@
// "menu.section.processes": "Processes",
"menu.section.processes": "Processus",
// "menu.section.health": "Health",
"menu.section.health": "Santé du système",
// "menu.section.registries": "Registries",
"menu.section.registries": "Registres",
@@ -4802,6 +4823,9 @@
// "default.search.results.head": "Search Results",
"default.search.results.head": "Résultats de recherche",
// "default-relationships.search.results.head": "Search Results",
"default-relationships.search.results.head": "Résultats de recherche",
// "search.sidebar.close": "Back to results",
"search.sidebar.close": "Retour aux résultats",
@@ -5796,7 +5820,8 @@
// "uploader.or": ", or ",
"uploader.or": ", ou ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "En cours de traitement",
// "uploader.queue-length": "Queue length",

View File

@@ -5980,7 +5980,8 @@
// "uploader.or": ", or ",
"uploader.or": ", no ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "A' pròiseasadh",
// "uploader.queue-length": "Queue length",

4536
src/assets/i18n/hi.json5 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5072,7 +5072,8 @@
// TODO Source message changed - Revise the translation
"uploader.or": ", vagy",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Feldolgozás",
// "uploader.queue-length": "Queue length",

View File

@@ -6651,9 +6651,9 @@
// TODO New key - Add a translation
"uploader.or": ", or ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO New key - Add a translation
"uploader.processing": "Processing",
"uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// "uploader.queue-length": "Queue length",
// TODO New key - Add a translation

View File

@@ -7135,7 +7135,8 @@
// "uploader.or": ", or ",
"uploader.or": ", немесе ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Өңдеу",
// "uploader.queue-length": "Queue length",

View File

@@ -5534,7 +5534,8 @@
// TODO Source message changed - Revise the translation
"uploader.or": ", vai",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Datu apstrāde",
// "uploader.queue-length": "Queue length",

View File

@@ -5907,7 +5907,8 @@
// TODO Source message changed - Revise the translation
"uploader.or": ", of",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Bezig",
// "uploader.queue-length": "Queue length",

View File

@@ -6651,9 +6651,9 @@
// TODO New key - Add a translation
"uploader.or": ", or ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO New key - Add a translation
"uploader.processing": "Processing",
"uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// "uploader.queue-length": "Queue length",
// TODO New key - Add a translation

View File

@@ -1,10 +1,10 @@
{
// "401.help": "You're not authorized to access this page. You can use the button below to get back to the home page.",
"401.help": "Você não está autorizado a acessar esta página. Você pode usar o botão abaixo para voltar à página inicial.",
"401.help": "Você não tem autorização para acessar esta página. Clique no botão abaixo para ir à página inicial.",
// "401.link.home-page": "Take me to the home page",
"401.link.home-page": "Leve-me para a página inicial",
"401.link.home-page": "Ir à página inicial",
// "401.unauthorized": "unauthorized",
"401.unauthorized": "não autorizado",
@@ -12,10 +12,10 @@
// "403.help": "You don't have permission to access this page. You can use the button below to get back to the home page.",
"403.help": "Você não tem permissão para acessar esta página. Você pode usar o botão abaixo para voltar à página inicial.",
"403.help": "Você não tem permissão para acessar esta página. Clique no botão abaixo para ir à página inicial.",
// "403.link.home-page": "Take me to the home page",
"403.link.home-page": "Leve-me para a página inicial",
"403.link.home-page": "Ir à página inicial",
// "403.forbidden": "forbidden",
"403.forbidden": "proibido",
@@ -27,14 +27,14 @@
"500.help": "O servidor está temporariamente impossibilitado de atender sua solicitação devido a um período de manutenção ou problemas no servidor. Por favor, tente novamente mais tarde.",
// "500.link.home-page": "Take me to the home page",
"500.link.home-page": "Leve-me para a página inicial",
"500.link.home-page": "Ir à página inicial",
// "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
"404.help": "Não pudemos encontrar a página pela qual procura. A página pode ter sido movida ou apagada. Você pode utilizar o botão abaixo para voltar a página inicial. ",
"404.help": "Não encontramos a página que você procura. A página pode ter sido movida ou apagada. Você pode utilizar o botão abaixo para voltar à página inicial. ",
// "404.link.home-page": "Take me to the home page",
"404.link.home-page": "Leve-me a página inicial",
"404.link.home-page": "Ir à página inicial",
// "404.page-not-found": "page not found",
"404.page-not-found": "página não encontrada",
@@ -49,54 +49,43 @@
"error-page.description.500": "Serviço Indisponível",
// "error-page.description.404": "page not found",
"error-page.description.404": "pagína não encontrada",
"error-page.description.404": "página não encontrada",
// "error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator",
// TODO New key - Add a translation
"error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator",
"error-page.orcid.generic-error": "Um erro ocorreu durante o login via ORCID. Certifique-se de que compartilhou o e-mail da sua conta ORCID com o DSpace. Se o erro persistir, contate o administrador.",
// "access-status.embargo.listelement.badge": "Embargo",
// TODO New key - Add a translation
"access-status.embargo.listelement.badge": "Embargo",
// "access-status.metadata.only.listelement.badge": "Metadata only",
// TODO New key - Add a translation
"access-status.metadata.only.listelement.badge": "Metadata only",
"access-status.metadata.only.listelement.badge": "Somente metadados",
// "access-status.open.access.listelement.badge": "Open Access",
// TODO New key - Add a translation
"access-status.open.access.listelement.badge": "Open Access",
"access-status.open.access.listelement.badge": "Acesso aberto",
// "access-status.restricted.listelement.badge": "Restricted",
// TODO New key - Add a translation
"access-status.restricted.listelement.badge": "Restricted",
"access-status.restricted.listelement.badge": "Restrito",
// "access-status.unknown.listelement.badge": "Unknown",
// TODO New key - Add a translation
"access-status.unknown.listelement.badge": "Unknown",
"access-status.unknown.listelement.badge": "Desconhecido",
// "admin.curation-tasks.breadcrumbs": "System curation tasks",
// TODO New key - Add a translation
"admin.curation-tasks.breadcrumbs": "System curation tasks",
"admin.curation-tasks.breadcrumbs": "Tarefas de curadoria do sistema",
// "admin.curation-tasks.title": "System curation tasks",
// TODO New key - Add a translation
"admin.curation-tasks.title": "System curation tasks",
"admin.curation-tasks.title": "Tarefas de curadoria do sistema",
// "admin.curation-tasks.header": "System curation tasks",
// TODO New key - Add a translation
"admin.curation-tasks.header": "System curation tasks",
"admin.curation-tasks.header": "Tarefas de curadoria do sistema",
// "admin.registries.bitstream-formats.breadcrumbs": "Format registry",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.breadcrumbs": "Format registry",
"admin.registries.bitstream-formats.breadcrumbs": "Registro de formato",
// "admin.registries.bitstream-formats.create.breadcrumbs": "Bitstream format",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.create.breadcrumbs": "Bitstream format",
"admin.registries.bitstream-formats.create.breadcrumbs": "Formato de bitstream",
// "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.",
"admin.registries.bitstream-formats.create.failure.content": "Um erro ocorreu durante a criação do novo formato de bitstream.",
"admin.registries.bitstream-formats.create.failure.content": "Ocorreu um erro durante a criação do novo formato de bitstream.",
// "admin.registries.bitstream-formats.create.failure.head": "Failure",
"admin.registries.bitstream-formats.create.failure.head": "Falha",
@@ -129,8 +118,7 @@
"admin.registries.bitstream-formats.description": "Esta lista de formatos de bitstream provê informações sobre formatos conhecidos e seus níveis de suporte.",
// "admin.registries.bitstream-formats.edit.breadcrumbs": "Bitstream format",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.edit.breadcrumbs": "Bitstream format",
"admin.registries.bitstream-formats.edit.breadcrumbs": "Formato de bitstream",
// "admin.registries.bitstream-formats.edit.description.hint": "",
"admin.registries.bitstream-formats.edit.description.hint": "",
@@ -139,7 +127,7 @@
"admin.registries.bitstream-formats.edit.description.label": "Descrição",
// "admin.registries.bitstream-formats.edit.extensions.hint": "Extensions are file extensions that are used to automatically identify the format of uploaded files. You can enter several extensions for each format.",
"admin.registries.bitstream-formats.edit.extensions.hint": "Extensões são extensões de arquivo que são usadas para identificar automaticamente o formato dos arquivo enviados. Você pode informar várias extensões para cada formato.",
"admin.registries.bitstream-formats.edit.extensions.hint": "Extensões de arquivo são usadas para identificar automaticamente o formato dos arquivo enviados. Você pode informar várias extensões para cada formato.",
// "admin.registries.bitstream-formats.edit.extensions.label": "File extensions",
"admin.registries.bitstream-formats.edit.extensions.label": "Extensões de arquivo",
@@ -157,7 +145,7 @@
"admin.registries.bitstream-formats.edit.head": "Formato de bitstream: {{ format }}",
// "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are hidden from the user, and used for administrative purposes.",
"admin.registries.bitstream-formats.edit.internal.hint": "Formatos marcados como interno são ocultos para o usuário, e utilizados por motivos administrativos.",
"admin.registries.bitstream-formats.edit.internal.hint": "Formatos marcados como interno são ocultados para o usuário e utilizados para propósitos administrativos.",
// "admin.registries.bitstream-formats.edit.internal.label": "Internal",
"admin.registries.bitstream-formats.edit.internal.label": "Interno",
@@ -175,13 +163,13 @@
"admin.registries.bitstream-formats.edit.shortDescription.label": "Nome",
// "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.",
"admin.registries.bitstream-formats.edit.success.content": "O formato de bitstream foi editedo com sucesso.",
"admin.registries.bitstream-formats.edit.success.content": "O formato de bitstream foi editado com sucesso.",
// "admin.registries.bitstream-formats.edit.success.head": "Success",
"admin.registries.bitstream-formats.edit.success.head": "Sucesso",
// "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.",
"admin.registries.bitstream-formats.edit.supportLevel.hint": "O nível de suporte que a sua instituição promete para este formato.",
"admin.registries.bitstream-formats.edit.supportLevel.hint": "O nível de suporte que a sua instituição garante para este formato.",
// "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level",
"admin.registries.bitstream-formats.edit.supportLevel.label": "Nível de suporte",
@@ -196,7 +184,7 @@
"admin.registries.bitstream-formats.table.delete": "Apagar selecionado(s)",
// "admin.registries.bitstream-formats.table.deselect-all": "Deselect all",
"admin.registries.bitstream-formats.table.deselect-all": "Desselecionar todos",
"admin.registries.bitstream-formats.table.deselect-all": "Desmarcar todos",
// "admin.registries.bitstream-formats.table.internal": "internal",
"admin.registries.bitstream-formats.table.internal": "Interno",
@@ -208,7 +196,6 @@
"admin.registries.bitstream-formats.table.name": "Nome",
// "admin.registries.bitstream-formats.table.return": "Back",
// TODO Source message changed - Revise the translation
"admin.registries.bitstream-formats.table.return": "Voltar",
// "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known",
@@ -224,17 +211,15 @@
"admin.registries.bitstream-formats.table.supportLevel.head": "Nível de Suporte",
// "admin.registries.bitstream-formats.title": "Bitstream Format Registry",
// TODO Source message changed - Revise the translation
"admin.registries.bitstream-formats.title": "DSpace Angular :: Registro de Formato de Bitstream",
// "admin.registries.metadata.breadcrumbs": "Metadata registry",
// TODO New key - Add a translation
"admin.registries.metadata.breadcrumbs": "Metadata registry",
"admin.registries.metadata.breadcrumbs": "Registro de metadados",
// "admin.registries.metadata.description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.",
"admin.registries.metadata.description": "O registro de metadados mantém a lista de todos os campos de metadados disponíveis no repositório. Estes campos podêm ser divididos em multiplos esquemas. Entretanto, o DSpace requer esquemas de Dublin Core qualificados.",
"admin.registries.metadata.description": "O registro de metadados mantém a lista de todos os campos de metadados disponíveis no repositório. Estes campos podem ser divididos em múltiplos esquemas. Entretanto, o DSpace requer esquemas de Dublin Core qualificados.",
// "admin.registries.metadata.form.create": "Create metadata schema",
"admin.registries.metadata.form.create": "Criar esquema de metadados",
@@ -252,7 +237,7 @@
"admin.registries.metadata.head": "Registro de Metadados",
// "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.",
"admin.registries.metadata.schemas.no-items": "Nenhum esquema de metadados a mostrar.",
"admin.registries.metadata.schemas.no-items": "Nenhum esquema de metadados a exibir.",
// "admin.registries.metadata.schemas.table.delete": "Delete selected",
"admin.registries.metadata.schemas.table.delete": "Apagar selecionado(s)",
@@ -267,14 +252,12 @@
"admin.registries.metadata.schemas.table.namespace": "Namespace",
// "admin.registries.metadata.title": "Metadata Registry",
// TODO Source message changed - Revise the translation
"admin.registries.metadata.title": "DSpace Angular :: Registro de Metadados",
// "admin.registries.schema.breadcrumbs": "Metadata schema",
// TODO New key - Add a translation
"admin.registries.schema.breadcrumbs": "Metadata schema",
"admin.registries.schema.breadcrumbs": "Esquema de metadados",
// "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".",
"admin.registries.schema.description": "Este é o esquema de metadados para \"{{namespace}}\".",
@@ -313,107 +296,92 @@
"admin.registries.schema.head": "Esquema de Metadados",
// "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"",
"admin.registries.schema.notification.created": "Criou o esquema de metadados \"{{prefix}}\" com sucesso",
"admin.registries.schema.notification.created": "Esquema de metadados \"{{prefix}}\" criado com sucesso",
// "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas",
"admin.registries.schema.notification.deleted.failure": "Falhou ao apagar {{amount}} esquema(s) de metadados",
"admin.registries.schema.notification.deleted.failure": "Falha ao apagar {{amount}} esquema(s) de metadados",
// "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas",
"admin.registries.schema.notification.deleted.success": "Apagou {{amount}} esquema(s) de metadados com sucesso",
// "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"",
"admin.registries.schema.notification.edited": "Editou o esquema de metadados \"{{prefix}}\" com sucesso",
"admin.registries.schema.notification.edited": "Esquema de metadados \"{{prefix}}\" editado com sucesso",
// "admin.registries.schema.notification.failure": "Error",
"admin.registries.schema.notification.failure": "Erro",
// "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"",
"admin.registries.schema.notification.field.created": "Criou o campo de medado \"{{field}}\" com sucesso",
"admin.registries.schema.notification.field.created": "Campo de metadados \"{{field}}\" criado com sucesso",
// "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields",
"admin.registries.schema.notification.field.deleted.failure": "Falhou ao apagar {{amount}} campo(s) de metadados",
"admin.registries.schema.notification.field.deleted.failure": "Falha ao apagar {{amount}} campo(s) de metadados",
// "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields",
"admin.registries.schema.notification.field.deleted.success": "Apagou {{amount}} campo(s) de metadados com sucesso",
// "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"",
"admin.registries.schema.notification.field.edited": "Editou o campo de metadodo \"{{field}}\" com sucesso",
"admin.registries.schema.notification.field.edited": "Campo de metadados \"{{field}}\" editado com sucesso",
// "admin.registries.schema.notification.success": "Success",
"admin.registries.schema.notification.success": "Sucesso",
// "admin.registries.schema.return": "Back",
// TODO Source message changed - Revise the translation
"admin.registries.schema.return": "Voltar",
// "admin.registries.schema.title": "Metadata Schema Registry",
// TODO Source message changed - Revise the translation
"admin.registries.schema.title": "DSpace Angular :: Registro de Esquema de Metadados",
// "admin.access-control.epeople.actions.delete": "Delete EPerson",
// TODO New key - Add a translation
"admin.access-control.epeople.actions.delete": "Delete EPerson",
"admin.access-control.epeople.actions.delete": "Excluir EPerson",
// "admin.access-control.epeople.actions.impersonate": "Impersonate EPerson",
// TODO New key - Add a translation
"admin.access-control.epeople.actions.impersonate": "Impersonate EPerson",
"admin.access-control.epeople.actions.impersonate": "Assumir o papel do EPerson",
// "admin.access-control.epeople.actions.reset": "Reset password",
// TODO New key - Add a translation
"admin.access-control.epeople.actions.reset": "Reset password",
"admin.access-control.epeople.actions.reset": "Redefinir senha",
// "admin.access-control.epeople.actions.stop-impersonating": "Stop impersonating EPerson",
// TODO New key - Add a translation
"admin.access-control.epeople.actions.stop-impersonating": "Stop impersonating EPerson",
"admin.access-control.epeople.actions.stop-impersonating": "Parar de assumir o papel do EPerson",
// "admin.access-control.epeople.breadcrumbs": "EPeople",
// TODO New key - Add a translation
"admin.access-control.epeople.breadcrumbs": "EPeople",
// "admin.access-control.epeople.title": "EPeople",
// TODO New key - Add a translation
"admin.access-control.epeople.title": "EPeople",
// "admin.access-control.epeople.head": "EPeople",
// TODO New key - Add a translation
"admin.access-control.epeople.head": "EPeople",
// "admin.access-control.epeople.search.head": "Search",
// TODO New key - Add a translation
"admin.access-control.epeople.search.head": "Search",
"admin.access-control.epeople.search.head": "Busca",
// "admin.access-control.epeople.button.see-all": "Browse All",
"admin.access-control.epeople.button.see-all": "Pesquisar Todos",
// "admin.access-control.epeople.search.scope.metadata": "Metadata",
// TODO New key - Add a translation
"admin.access-control.epeople.search.scope.metadata": "Metadata",
"admin.access-control.epeople.search.scope.metadata": "Metadado",
// "admin.access-control.epeople.search.scope.email": "E-mail (exact)",
"admin.access-control.epeople.search.scope.email": "Email (exato)",
"admin.access-control.epeople.search.scope.email": "E-mail (exato)",
// "admin.access-control.epeople.search.button": "Search",
"admin.access-control.epeople.search.button": "Procurar",
// "admin.access-control.epeople.search.placeholder": "Search people...",
// TODO New key - Add a translation
"admin.access-control.epeople.search.placeholder": "Search people...",
"admin.access-control.epeople.search.placeholder": "Procurar pessoas...",
// "admin.access-control.epeople.button.add": "Add EPerson",
// TODO New key - Add a translation
"admin.access-control.epeople.button.add": "Add EPerson",
"admin.access-control.epeople.button.add": "Adicionar EPerson",
// "admin.access-control.epeople.table.id": "ID",
// TODO New key - Add a translation
"admin.access-control.epeople.table.id": "ID",
// "admin.access-control.epeople.table.name": "Name",
"admin.access-control.epeople.table.name": "Nome",
// "admin.access-control.epeople.table.email": "E-mail (exact)",
// TODO New key - Add a translation
"admin.access-control.epeople.table.email": "E-mail (exact)",
// "admin.access-control.epeople.table.edit": "Edit",
@@ -424,140 +392,108 @@
// "admin.access-control.epeople.table.edit.buttons.edit-disabled": "You are not authorized to edit this group",
// TODO New key - Add a translation
"admin.access-control.epeople.table.edit.buttons.edit-disabled": "You are not authorized to edit this group",
"admin.access-control.epeople.table.edit.buttons.edit-disabled": "Você não tem autorização para editar este grupo",
// "admin.access-control.epeople.table.edit.buttons.remove": "Delete \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.epeople.table.edit.buttons.remove": "Delete \"{{name}}\"",
"admin.access-control.epeople.table.edit.buttons.remove": "Excluir \"{{name}}\"",
// "admin.access-control.epeople.no-items": "No EPeople to show.",
// TODO New key - Add a translation
"admin.access-control.epeople.no-items": "No EPeople to show.",
"admin.access-control.epeople.no-items": "Nenhum EPeople a exibir.",
// "admin.access-control.epeople.form.create": "Create EPerson",
// TODO New key - Add a translation
"admin.access-control.epeople.form.create": "Create EPerson",
"admin.access-control.epeople.form.create": "Criar EPerson",
// "admin.access-control.epeople.form.edit": "Edit EPerson",
// TODO New key - Add a translation
"admin.access-control.epeople.form.edit": "Edit EPerson",
"admin.access-control.epeople.form.edit": "Editar EPerson",
// "admin.access-control.epeople.form.firstName": "First name",
// TODO New key - Add a translation
"admin.access-control.epeople.form.firstName": "First name",
"admin.access-control.epeople.form.firstName": "Primeiro nome",
// "admin.access-control.epeople.form.lastName": "Last name",
// TODO New key - Add a translation
"admin.access-control.epeople.form.lastName": "Last name",
"admin.access-control.epeople.form.lastName": "Último nome",
// "admin.access-control.epeople.form.email": "E-mail",
// TODO New key - Add a translation
"admin.access-control.epeople.form.email": "E-mail",
// "admin.access-control.epeople.form.emailHint": "Must be valid e-mail address",
// TODO New key - Add a translation
"admin.access-control.epeople.form.emailHint": "Must be valid e-mail address",
"admin.access-control.epeople.form.emailHint": "Deve ser um endereço de e-mail válido",
// "admin.access-control.epeople.form.canLogIn": "Can log in",
// TODO New key - Add a translation
"admin.access-control.epeople.form.canLogIn": "Can log in",
"admin.access-control.epeople.form.canLogIn": "Pode fazer login",
// "admin.access-control.epeople.form.requireCertificate": "Requires certificate",
// TODO New key - Add a translation
"admin.access-control.epeople.form.requireCertificate": "Requires certificate",
"admin.access-control.epeople.form.requireCertificate": "Requer certificado",
// "admin.access-control.epeople.form.return": "Back",
// TODO New key - Add a translation
"admin.access-control.epeople.form.return": "Back",
"admin.access-control.epeople.form.return": "Voltar",
// "admin.access-control.epeople.form.notification.created.success": "Successfully created EPerson \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.epeople.form.notification.created.success": "Successfully created EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.created.success": "EPerson \"{{name}}\" criado com sucesso",
// "admin.access-control.epeople.form.notification.created.failure": "Failed to create EPerson \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.epeople.form.notification.created.failure": "Failed to create EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.created.failure": "Falha ao criar EPerson \"{{name}}\"",
// "admin.access-control.epeople.form.notification.created.failure.emailInUse": "Failed to create EPerson \"{{name}}\", email \"{{email}}\" already in use.",
// TODO New key - Add a translation
"admin.access-control.epeople.form.notification.created.failure.emailInUse": "Failed to create EPerson \"{{name}}\", email \"{{email}}\" already in use.",
"admin.access-control.epeople.form.notification.created.failure.emailInUse": "Falha ao criar EPerson \"{{name}}\": e-mail \"{{email}}\" já está em uso.",
// "admin.access-control.epeople.form.notification.edited.failure.emailInUse": "Failed to edit EPerson \"{{name}}\", email \"{{email}}\" already in use.",
// TODO New key - Add a translation
"admin.access-control.epeople.form.notification.edited.failure.emailInUse": "Failed to edit EPerson \"{{name}}\", email \"{{email}}\" already in use.",
"admin.access-control.epeople.form.notification.edited.failure.emailInUse": "Falha ao editar EPerson \"{{name}}\": e-mail \"{{email}}\" já está em uso.",
// "admin.access-control.epeople.form.notification.edited.success": "Successfully edited EPerson \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.epeople.form.notification.edited.success": "Successfully edited EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.edited.success": "EPerson \"{{name}}\" editado com sucesso",
// "admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.edited.failure": "Falha ao editar EPerson \"{{name}}\"",
// "admin.access-control.epeople.form.notification.deleted.success": "Successfully deleted EPerson \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.epeople.form.notification.deleted.success": "Successfully deleted EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.deleted.success": "EPerson \"{{name}}\" excluído com sucesso",
// "admin.access-control.epeople.form.notification.deleted.failure": "Failed to delete EPerson \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.epeople.form.notification.deleted.failure": "Failed to delete EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.deleted.failure": "Falha ao excluir EPerson \"{{name}}\"",
// "admin.access-control.epeople.form.groupsEPersonIsMemberOf": "Member of these groups:",
// TODO New key - Add a translation
"admin.access-control.epeople.form.groupsEPersonIsMemberOf": "Member of these groups:",
"admin.access-control.epeople.form.groupsEPersonIsMemberOf": "Membro destes grupos:",
// "admin.access-control.epeople.form.table.id": "ID",
// TODO New key - Add a translation
"admin.access-control.epeople.form.table.id": "ID",
// "admin.access-control.epeople.form.table.name": "Name",
"admin.access-control.epeople.form.table.name": "Nome",
// "admin.access-control.epeople.form.table.collectionOrCommunity": "Collection/Community",
// TODO New key - Add a translation
"admin.access-control.epeople.form.table.collectionOrCommunity": "Collection/Community",
"admin.access-control.epeople.form.table.collectionOrCommunity": "Coleção/Comunidade",
// "admin.access-control.epeople.form.memberOfNoGroups": "This EPerson is not a member of any groups",
// TODO New key - Add a translation
"admin.access-control.epeople.form.memberOfNoGroups": "This EPerson is not a member of any groups",
"admin.access-control.epeople.form.memberOfNoGroups": "Este EPerson não é membro de nenhum grupo",
// "admin.access-control.epeople.form.goToGroups": "Add to groups",
// TODO New key - Add a translation
"admin.access-control.epeople.form.goToGroups": "Add to groups",
"admin.access-control.epeople.form.goToGroups": "Adicionar ao grupos",
// "admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"",
"admin.access-control.epeople.notification.deleted.failure": "Falha ao excluir EPerson: \"{{name}}\"",
// "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"",
"admin.access-control.epeople.notification.deleted.success": "EPerson: \"{{name}}\" excluído com sucesso",
// "admin.access-control.groups.title": "Groups",
// TODO New key - Add a translation
"admin.access-control.groups.title": "Groups",
"admin.access-control.groups.title": "Grupos",
// "admin.access-control.groups.breadcrumbs": "Groups",
// TODO New key - Add a translation
"admin.access-control.groups.breadcrumbs": "Groups",
"admin.access-control.groups.breadcrumbs": "Grupos",
// "admin.access-control.groups.singleGroup.breadcrumbs": "Edit Group",
// TODO New key - Add a translation
"admin.access-control.groups.singleGroup.breadcrumbs": "Edit Group",
"admin.access-control.groups.singleGroup.breadcrumbs": "Editar Grupo",
// "admin.access-control.groups.title.singleGroup": "Edit Group",
// TODO New key - Add a translation
"admin.access-control.groups.title.singleGroup": "Edit Group",
"admin.access-control.groups.title.singleGroup": "Editar Grupo",
// "admin.access-control.groups.title.addGroup": "New Group",
// TODO New key - Add a translation
"admin.access-control.groups.title.addGroup": "New Group",
"admin.access-control.groups.title.addGroup": "Novo Grupo",
// "admin.access-control.groups.addGroup.breadcrumbs": "New Group",
// TODO New key - Add a translation
"admin.access-control.groups.addGroup.breadcrumbs": "New Group",
"admin.access-control.groups.addGroup.breadcrumbs": "Novo Grupo",
// "admin.access-control.groups.head": "Groups",
"admin.access-control.groups.head": "Grupos",
@@ -569,46 +505,37 @@
"admin.access-control.groups.search.head": "Pesquisar grupos",
// "admin.access-control.groups.button.see-all": "Browse all",
// TODO New key - Add a translation
"admin.access-control.groups.button.see-all": "Browse all",
"admin.access-control.groups.button.see-all": "Pesquisar todos",
// "admin.access-control.groups.search.button": "Search",
// TODO New key - Add a translation
"admin.access-control.groups.search.button": "Search",
"admin.access-control.groups.search.button": "Procurar",
// "admin.access-control.groups.search.placeholder": "Search groups...",
// TODO New key - Add a translation
"admin.access-control.groups.search.placeholder": "Search groups...",
"admin.access-control.groups.search.placeholder": "Procurar grupos...",
// "admin.access-control.groups.table.id": "ID",
// TODO New key - Add a translation
"admin.access-control.groups.table.id": "ID",
// "admin.access-control.groups.table.name": "Name",
"admin.access-control.groups.table.name": "Nome",
// "admin.access-control.groups.table.collectionOrCommunity": "Collection/Community",
// TODO New key - Add a translation
"admin.access-control.groups.table.collectionOrCommunity": "Collection/Community",
"admin.access-control.groups.table.collectionOrCommunity": "Coleção/Comunidade",
// "admin.access-control.groups.table.members": "Members",
// TODO New key - Add a translation
"admin.access-control.groups.table.members": "Members",
"admin.access-control.groups.table.members": "Membros",
// "admin.access-control.groups.table.edit": "Edit",
"admin.access-control.groups.table.edit": "Editar",
// "admin.access-control.groups.table.edit.buttons.edit": "Edit \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.groups.table.edit.buttons.edit": "Edit \"{{name}}\"",
"admin.access-control.groups.table.edit.buttons.edit": "Editar \"{{name}}\"",
// "admin.access-control.groups.table.edit.buttons.remove": "Delete \"{{name}}\"",
// TODO New key - Add a translation
"admin.access-control.groups.table.edit.buttons.remove": "Delete \"{{name}}\"",
"admin.access-control.groups.table.edit.buttons.remove": "Excluir \"{{name}}\"",
// "admin.access-control.groups.no-items": "No groups found with this in their name or this as UUID",
// TODO New key - Add a translation
"admin.access-control.groups.no-items": "No groups found with this in their name or this as UUID",
"admin.access-control.groups.no-items": "Nenhum grupo encontrado com isto no seu nome ou com esse UUID",
// "admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"",
// TODO New key - Add a translation
@@ -7809,7 +7736,8 @@
// "uploader.or": ", or ",
"uploader.or": ", ou ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Processando",
// "uploader.queue-length": "Queue length",

View File

@@ -5264,7 +5264,8 @@
// "uploader.or": ", or ",
"uploader.or": ", ou",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "A Processar",
// "uploader.queue-length": "Queue length",

View File

@@ -1499,7 +1499,7 @@
"communityList.tabTitle": "Enheter",
// "communityList.title": "List of Communities",
"communityList.title": "Lista med enheter",
"communityList.title": "Lista med enheter",
// "communityList.showMore": "Show More",
"communityList.showMore": "Visa fler",
@@ -5468,7 +5468,7 @@
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.local-entity": "Lokal tidskrift har lagts till",
// "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.new-entity": "Successfully imported and added external journal to the selection",
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.new-entity": "Extern tidskrift har importerats och lagts till",
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.new-entity": "Extern tidskrift har importerats och lagts till",
// "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.title": "Import Remote Journal Issue",
"submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.title": "Importera externt tidskriftsexemplar",
@@ -6179,7 +6179,8 @@
// "uploader.or": ", or ",
"uploader.or": ", eller ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Bearbetar",
// "uploader.queue-length": "Queue length",

View File

@@ -6651,9 +6651,9 @@
// TODO New key - Add a translation
"uploader.or": ", or ",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO New key - Add a translation
"uploader.processing": "Processing",
"uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// "uploader.queue-length": "Queue length",
// TODO New key - Add a translation

View File

@@ -5052,7 +5052,8 @@
// "uploader.or": ", or ",
"uploader.or": ", veya",
// "uploader.processing": "Processing",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "İşleniyor",
// "uploader.queue-length": "Queue length",

View File

@@ -203,6 +203,7 @@ export class DefaultAppConfig implements AppConfig {
{ code: 'tr', label: 'Türkçe', active: true },
{ code: 'kk', label: 'Қазақ', active: true },
{ code: 'bn', label: 'বাংলা', active: true },
{ code: 'hi', label: 'हिंदी', active: true},
{ code: 'el', label: 'Ελληνικά', active: true }
];