diff --git a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts b/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts index 83f67a770e..5af18c778f 100644 --- a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts +++ b/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts @@ -1,11 +1,32 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; +import { GroupFormComponent } from './group-registry/group-form/group-form.component'; +import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; +import { URLCombiner } from '../../core/url-combiner/url-combiner'; +import { getAccessControlModulePath } from '../admin-routing.module'; + +const GROUP_EDIT_PATH = 'groups'; + +export function getGroupEditPath(id: string) { + return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString(); +} @NgModule({ imports: [ RouterModule.forChild([ { path: 'epeople', component: EPeopleRegistryComponent, data: { title: 'admin.access-control.epeople.title' } }, + { path: GROUP_EDIT_PATH, component: GroupsRegistryComponent, data: { title: 'admin.access-control.groups.title' } }, + { + path: `${GROUP_EDIT_PATH}/:groupId`, + component: GroupFormComponent, + data: {title: 'admin.registries.schema.title'} + }, + { + path: `${GROUP_EDIT_PATH}/newGroup`, + component: GroupFormComponent, + data: {title: 'admin.registries.schema.title'} + }, ]) ] }) diff --git a/src/app/+admin/admin-access-control/admin-access-control.module.ts b/src/app/+admin/admin-access-control/admin-access-control.module.ts index 0c8573e135..8b8ad2a420 100644 --- a/src/app/+admin/admin-access-control/admin-access-control.module.ts +++ b/src/app/+admin/admin-access-control/admin-access-control.module.ts @@ -6,6 +6,10 @@ import { SharedModule } from '../../shared/shared.module'; import { AdminAccessControlRoutingModule } from './admin-access-control-routing.module'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; +import { GroupFormComponent } from './group-registry/group-form/group-form.component'; +import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component'; +import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component'; +import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; @NgModule({ imports: [ @@ -17,7 +21,11 @@ import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-fo ], declarations: [ EPeopleRegistryComponent, - EPersonFormComponent + EPersonFormComponent, + GroupsRegistryComponent, + GroupFormComponent, + SubgroupsListComponent, + MembersListComponent ], entryComponents: [] }) diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.html b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.html index dd1e8bb62c..20593756c1 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.html @@ -15,7 +15,10 @@ - +
+ + + +
+
+
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{ePerson.id}}{{ePerson.name}} +
+ + + +
+
+
+ +
+ + + +

{{messagePrefix + '.headMembers' | translate}}

+ + + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{ePerson.id}}{{ePerson.name}} +
+ +
+
+
+ +
+ + + + diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.spec.ts b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.spec.ts new file mode 100644 index 0000000000..07fab49ca5 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.spec.ts @@ -0,0 +1,241 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule, By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { RestResponse } from '../../../../../core/cache/response.models'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../../core/eperson/group-data.service'; +import { EPerson } from '../../../../../core/eperson/models/eperson.model'; +import { Group } from '../../../../../core/eperson/models/group.model'; +import { PageInfo } from '../../../../../core/shared/page-info.model'; +import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; +import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service'; +import { MockRouter } from '../../../../../shared/mocks/mock-router'; +import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson-mock'; +import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; +import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader'; +import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service-stub'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { MembersListComponent } from './members-list.component'; + +describe('MembersListComponent', () => { + let component: MembersListComponent; + let fixture: ComponentFixture; + let translateService: TranslateService; + let builderService: FormBuilderService; + let ePersonDataServiceStub: any; + let groupsDataServiceStub: any; + let activeGroup; + let allEPersons; + let allGroups; + let epersonMembers; + let subgroupMembers; + + beforeEach(async(() => { + activeGroup = GroupMock; + epersonMembers = [EPersonMock2]; + subgroupMembers = [GroupMock2]; + allEPersons = [EPersonMock, EPersonMock2]; + allGroups = [GroupMock, GroupMock2]; + ePersonDataServiceStub = { + activeGroup: activeGroup, + epersonMembers: epersonMembers, + subgroupMembers: subgroupMembers, + findAllByHref(href: string): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), groupsDataServiceStub.getEPersonMembers())) + }, + searchByScope(scope: string, query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allEPersons)) + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + }, + clearEPersonRequests() { + // empty + }, + clearLinkRequests() { + // empty + }, + getEPeoplePageRouterLink(): string { + return '/admin/access-control/epeople'; + } + }; + groupsDataServiceStub = { + activeGroup: activeGroup, + epersonMembers: epersonMembers, + subgroupMembers: subgroupMembers, + allGroups: allGroups, + getActiveGroup(): Observable { + return observableOf(activeGroup); + }, + getEPersonMembers() { + return this.epersonMembers; + }, + searchGroups(query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this.allGroups)) + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + }, + addMemberToGroup(parentGroup, eperson: EPerson): Observable { + this.epersonMembers = [...this.epersonMembers, eperson]; + return observableOf(new RestResponse(true, 200, 'Success')); + }, + clearGroupsRequests() { + // empty + }, + clearGroupLinkRequests() { + // empty + }, + getGroupEditPageRouterLink(group: Group): string { + return '/admin/access-control/groups/' + group.id; + }, + deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable { + this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => { + if (eperson.id !== epersonToDelete.id) { + return eperson; + } + }); + if (this.epersonMembers === undefined) { + this.epersonMembers = [] + } + return observableOf(new RestResponse(true, 200, 'Success')); + } + }; + builderService = getMockFormBuilderService(); + translateService = getMockTranslateService(); + TestBed.configureTestingModule({ + imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + declarations: [MembersListComponent], + providers: [MembersListComponent, + { provide: EPersonDataService, useValue: ePersonDataServiceStub }, + { provide: GroupDataService, useValue: groupsDataServiceStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: FormBuilderService, useValue: builderService }, + { provide: Router, useValue: new MockRouter() }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MembersListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + afterEach(fakeAsync(() => { + fixture.destroy(); + flush(); + component = null; + })); + + it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => { + expect(comp).toBeDefined(); + })); + + it('should show list of eperson members of current active group', () => { + const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child')); + expect(epersonIdsFound.length).toEqual(1); + epersonMembers.map((eperson: EPerson) => { + expect(epersonIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === eperson.uuid); + })).toBeTruthy(); + }); + }); + + describe('search', () => { + describe('when searching without query', () => { + let epersonsFound; + beforeEach(fakeAsync(() => { + component.search({ scope: 'metadata', query: '' }); + tick(); + fixture.detectChanges(); + epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); + })); + + it('should display all epersons', () => { + expect(epersonsFound.length).toEqual(2); + }); + + describe('if eperson is already a eperson', () => { + it('should have delete button, else it should have add button', () => { + activeGroup.epersons.map((eperson: EPerson) => { + epersonsFound.map((foundEPersonRowElement) => { + if (foundEPersonRowElement.debugElement !== undefined) { + const epersonId = foundEPersonRowElement.debugElement.query(By.css('td:first-child')); + const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + if (epersonId.nativeElement.textContent === eperson.id) { + expect(addButton).toBeUndefined(); + expect(deleteButton).toBeDefined(); + } else { + expect(deleteButton).toBeUndefined(); + expect(addButton).toBeDefined(); + } + } + }) + }) + }); + }); + + describe('if first add button is pressed', () => { + beforeEach(fakeAsync(() => { + const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus')); + addButton.nativeElement.click(); + tick(); + fixture.detectChanges(); + })); + it('all groups in search member of selected group', () => { + epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); + expect(epersonsFound.length).toEqual(2); + epersonsFound.map((foundEPersonRowElement) => { + if (foundEPersonRowElement.debugElement !== undefined) { + const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).toBeUndefined(); + expect(deleteButton).toBeDefined(); + } + }) + }); + }); + + describe('if first delete button is pressed', () => { + beforeEach(fakeAsync(() => { + const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt')); + addButton.nativeElement.click(); + tick(); + fixture.detectChanges(); + })); + it('first eperson in search delete button, because now member', () => { + epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); + epersonsFound.map((foundEPersonRowElement) => { + if (foundEPersonRowElement.debugElement !== undefined) { + const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + expect(deleteButton).toBeUndefined(); + expect(addButton).toBeDefined(); + } + }) + }); + }); + }); + }); + +}); diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts new file mode 100644 index 0000000000..b2e9ea75d6 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts @@ -0,0 +1,247 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { map, mergeMap, take } from 'rxjs/operators'; +import { RestResponse } from '../../../../../core/cache/response.models'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../../core/eperson/group-data.service'; +import { EPerson } from '../../../../../core/eperson/models/eperson.model'; +import { Group } from '../../../../../core/eperson/models/group.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { hasValue } from '../../../../../shared/empty.util'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; + +@Component({ + selector: 'ds-members-list', + templateUrl: './members-list.component.html' +}) +/** + * The list of members in the edit group page + */ +export class MembersListComponent implements OnInit, OnDestroy { + + @Input() + messagePrefix: string; + + /** + * EPeople being displayed in search result, initially all members, after search result of search + */ + ePeopleSearch: Observable>>; + /** + * List of EPeople members of currently active group being edited + */ + ePeopleMembersOfGroup: Observable>>; + + /** + * Pagination config used to display the list of EPeople that are result of EPeople search + */ + configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'search-members-list-pagination', + pageSize: 5, + currentPage: 1 + }); + /** + * Pagination config used to display the list of EPerson Membes of active group being edited + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'members-list-pagination', + pageSize: 5, + currentPage: 1 + }); + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + + // The search form + searchForm; + + // Current search in edit group - epeople search form + currentSearchQuery: string; + currentSearchScope: string; + + // Whether or not user has done a EPeople search yet + searchDone: boolean; + + // current active group being edited + groupBeingEdited: Group; + + constructor(private groupDataService: GroupDataService, + public ePersonDataService: EPersonDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder, + private router: Router) { + this.currentSearchQuery = ''; + this.currentSearchScope = 'metadata'; + } + + ngOnInit() { + this.searchForm = this.formBuilder.group(({ + scope: 'metadata', + query: '', + })); + this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupBeingEdited = activeGroup; + this.forceUpdateEPeople(activeGroup); + } + })); + } + + /** + * Event triggered when the user changes page on search result + * @param event + */ + onPageChangeSearch(event) { + this.configSearch.currentPage = event; + this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery }); + } + + /** + * Event triggered when the user changes page on EPerson embers of active group + * @param event + */ + onPageChange(event) { + this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, { + currentPage: event, + elementsPerPage: this.config.pageSize + }) + } + + /** + * Deletes a given EPerson from the members list of the group currently being edited + * @param ePerson EPerson we want to delete as member from group that is currently being edited + */ + deleteMemberFromGroup(ePerson: EPerson) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson); + this.showNotifications('deleteMember', response, ePerson.name, activeGroup); + this.forceUpdateEPeople(activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); + } + + /** + * Adds a given EPerson to the members list of the group currently being edited + * @param ePerson EPerson we want to add as member to group that is currently being edited + */ + addMemberToGroup(ePerson: EPerson) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson); + this.showNotifications('addMember', response, ePerson.name, activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); + this.forceUpdateEPeople(this.groupBeingEdited, ePerson); + } + + /** + * Whether or not the given ePerson is a member of the group currently being edited + * @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited + */ + isMemberOfGroup(possibleMember: EPerson): Observable { + return this.groupDataService.getActiveGroup().pipe(take(1), + mergeMap((group: Group) => { + if (group != null) { + return this.ePersonDataService.findAllByHref(group._links.epersons.href, { + currentPage: 0, + elementsPerPage: Number.MAX_SAFE_INTEGER + }) + .pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((listEPeopleInGroup: PaginatedList) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)), + map((epeople: EPerson[]) => epeople.length > 0)) + } else { + return observableOf(false); + } + })) + } + + /** + * Search in the EPeople by name, email or metadata + * @param data Contains scope and query param + */ + search(data: any) { + const query: string = data.query; + const scope: string = data.scope; + if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) { + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); + this.currentSearchQuery = query; + this.configSearch.currentPage = 1; + } + if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) { + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); + this.currentSearchScope = scope; + this.configSearch.currentPage = 1; + } + this.searchDone = true; + this.ePeopleSearch = this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + currentPage: this.configSearch.currentPage, + elementsPerPage: this.configSearch.pageSize + }); + } + + /** + * Force-update the list of EPeople by first clearing the cache related to EPeople, then performing + * a new REST call + * @param activeGroup Group currently being edited + */ + public forceUpdateEPeople(activeGroup: Group, ePersonToUpdate?: EPerson) { + if (ePersonToUpdate != null) { + this.ePersonDataService.clearLinkRequests(ePersonToUpdate._links.groups.href); + } + this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href); + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup)); + this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(activeGroup._links.epersons.href, { + currentPage: this.configSearch.currentPage, + elementsPerPage: this.configSearch.pageSize + }) + } + + /** + * unsub all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } + + /** + * Shows a notification based on the success/failure of the request + * @param messageSuffix Suffix for message + * @param response RestResponse observable containing success/failure request + * @param nameObject Object request was about + * @param activeGroup Group currently being edited + */ + showNotifications(messageSuffix: string, response: Observable, nameObject: string, activeGroup: Group) { + response.pipe(take(1)).subscribe((restResponse: RestResponse) => { + if (restResponse.isSuccessful) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject })); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject })); + } + }) + } + + /** + * Reset all input-fields to be empty and search all search + */ + clearFormAndResetResult() { + this.searchForm.patchValue({ + query: '', + }); + this.search({ query: '' }); + } +} diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html new file mode 100644 index 0000000000..9558b9da98 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html @@ -0,0 +1,117 @@ + +

{{messagePrefix + '.head' | translate}}

+ + +
+
+
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{group.id}}{{group.name}} +
+ + +

{{ messagePrefix + '.table.edit.currentGroup' | translate }}

+ + +
+
+
+
+ + + +

{{messagePrefix + '.headSubgroups' | translate}}

+ + + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{group.id}}{{group.name}} +
+ +
+
+
+
+ + + +
diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts new file mode 100644 index 0000000000..4bd088b513 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts @@ -0,0 +1,208 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule, By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { RestResponse } from '../../../../../core/cache/response.models'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { GroupDataService } from '../../../../../core/eperson/group-data.service'; +import { Group } from '../../../../../core/eperson/models/group.model'; +import { PageInfo } from '../../../../../core/shared/page-info.model'; +import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; +import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service'; +import { MockRouter } from '../../../../../shared/mocks/mock-router'; +import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; +import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader'; +import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service-stub'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { SubgroupsListComponent } from './subgroups-list.component'; + +describe('SubgroupsListComponent', () => { + let component: SubgroupsListComponent; + let fixture: ComponentFixture; + let translateService: TranslateService; + let builderService: FormBuilderService; + let ePersonDataServiceStub: any; + let groupsDataServiceStub: any; + let activeGroup; + let subgroups; + let allGroups; + let routerStub; + + beforeEach(async(() => { + activeGroup = GroupMock; + subgroups = [GroupMock2]; + allGroups = [GroupMock, GroupMock2]; + ePersonDataServiceStub = {}; + groupsDataServiceStub = { + activeGroup: activeGroup, + subgroups: subgroups, + getActiveGroup(): Observable { + return observableOf(this.activeGroup); + }, + getSubgroups(): Group { + return this.activeGroup; + }, + findAllByHref(href: string): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this.subgroups)) + }, + getGroupEditPageRouterLink(group: Group): string { + return '/admin/access-control/groups/' + group.id; + }, + searchGroups(query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allGroups)) + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + }, + addSubGroupToGroup(parentGroup, subgroup: Group): Observable { + this.subgroups = [...this.subgroups, subgroup]; + return observableOf(new RestResponse(true, 200, 'Success')); + }, + clearGroupsRequests() { + // empty + }, + clearGroupLinkRequests() { + // empty + }, + deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable { + this.subgroups = this.subgroups.find((group: Group) => { + if (group.id !== subgroup.id) { + return group; + } + }); + return observableOf(new RestResponse(true, 200, 'Success')); + } + }; + routerStub = new MockRouter(); + builderService = getMockFormBuilderService(); + translateService = getMockTranslateService(); + TestBed.configureTestingModule({ + imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + declarations: [SubgroupsListComponent], + providers: [SubgroupsListComponent, + { provide: GroupDataService, useValue: groupsDataServiceStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: FormBuilderService, useValue: builderService }, + { provide: Router, useValue: routerStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SubgroupsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + afterEach(fakeAsync(() => { + fixture.destroy(); + flush(); + component = null; + })); + + it('should create SubgroupsListComponent', inject([SubgroupsListComponent], (comp: SubgroupsListComponent) => { + expect(comp).toBeDefined(); + })); + + it('should show list of subgroups of current active group', () => { + const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child')); + expect(groupIdsFound.length).toEqual(1); + activeGroup.subgroups.map((group: Group) => { + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === group.uuid); + })).toBeTruthy(); + }) + }); + + describe('if first group delete button is pressed', () => { + let groupsFound; + beforeEach(fakeAsync(() => { + const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton')); + addButton.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => { + groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr')); + expect(groupsFound.length).toEqual(0); + }); + }); + + describe('search', () => { + describe('when searching with empty query', () => { + let groupsFound; + beforeEach(fakeAsync(() => { + component.search({ query: '' }); + groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); + })); + + it('should display all groups', () => { + fixture.detectChanges(); + groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); + expect(groupsFound.length).toEqual(2); + groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); + const groupIdsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child')); + allGroups.map((group: Group) => { + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === group.uuid); + })).toBeTruthy(); + }) + }); + + describe('if group is already a subgroup', () => { + it('should have delete button, else it should have add button', () => { + fixture.detectChanges(); + groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); + const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups; + if (getSubgroups !== undefined && getSubgroups.length > 0) { + groupsFound.map((foundGroupRowElement) => { + if (foundGroupRowElement.debugElement !== undefined) { + const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).toBeUndefined(); + expect(deleteButton).toBeDefined(); + } + }) + } else { + getSubgroups.map((group: Group) => { + groupsFound.map((foundGroupRowElement) => { + if (foundGroupRowElement.debugElement !== undefined) { + const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child')); + const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + if (groupId.nativeElement.textContent === group.id) { + expect(addButton).toBeUndefined(); + expect(deleteButton).toBeDefined(); + } else { + expect(deleteButton).toBeUndefined(); + expect(addButton).toBeDefined(); + } + } + }) + }) + } + }); + }); + }); + }); + +}); diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts new file mode 100644 index 0000000000..62927b74aa --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -0,0 +1,253 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { map, mergeMap, take } from 'rxjs/operators'; +import { RestResponse } from '../../../../../core/cache/response.models'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { GroupDataService } from '../../../../../core/eperson/group-data.service'; +import { Group } from '../../../../../core/eperson/models/group.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { hasValue } from '../../../../../shared/empty.util'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; + +@Component({ + selector: 'ds-subgroups-list', + templateUrl: './subgroups-list.component.html' +}) +/** + * The list of subgroups in the edit group page + */ +export class SubgroupsListComponent implements OnInit, OnDestroy { + + @Input() + messagePrefix: string; + + /** + * Result of search groups, initially all groups + */ + groupsSearch: Observable>>; + /** + * List of all subgroups of group being edited + */ + subgroupsOfGroup: Observable>>; + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + + /** + * Pagination config used to display the list of groups that are result of groups search + */ + configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'search-subgroups-list-pagination', + pageSize: 5, + currentPage: 1 + }); + /** + * Pagination config used to display the list of subgroups of currently active group being edited + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'subgroups-list-pagination', + pageSize: 5, + currentPage: 1 + }); + + // The search form + searchForm; + + // Current search in edit group - groups search form + currentSearchQuery: string; + + // Whether or not user has done a Groups search yet + searchDone: boolean; + + // current active group being edited + groupBeingEdited: Group; + + constructor(public groupDataService: GroupDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder, + private router: Router) { + this.currentSearchQuery = ''; + } + + ngOnInit() { + this.searchForm = this.formBuilder.group(({ + query: '', + })); + this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupBeingEdited = activeGroup; + this.forceUpdateGroups(activeGroup); + } + })); + } + + /** + * Event triggered when the user changes page on search result + * @param event + */ + onPageChangeSearch(event) { + this.configSearch.currentPage = event; + this.search({ query: this.currentSearchQuery }); + } + + /** + * Event triggered when the user changes page on subgroups of active group + * @param event + */ + onPageChange(event) { + this.subgroupsOfGroup = this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, { + currentPage: event, + elementsPerPage: this.config.pageSize + }); + } + + /** + * Whether or not the given group is a subgroup of the group currently being edited + * @param possibleSubgroup Group that is a possible subgroup (being tested) of the group currently being edited + */ + isSubgroupOfGroup(possibleSubgroup: Group): Observable { + return this.groupDataService.getActiveGroup().pipe(take(1), + mergeMap((activeGroup: Group) => { + if (activeGroup != null) { + if (activeGroup.uuid === possibleSubgroup.uuid) { + return observableOf(false); + } else { + return this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, { + currentPage: 0, + elementsPerPage: Number.MAX_SAFE_INTEGER + }) + .pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((listTotalGroups: PaginatedList) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === possibleSubgroup.id)), + map((groups: Group[]) => groups.length > 0)) + } + } else { + return observableOf(false); + } + })); + } + + /** + * Whether or not the given group is the current group being edited + * @param group Group that is possibly the current group being edited + */ + isActiveGroup(group: Group): Observable { + return this.groupDataService.getActiveGroup().pipe(take(1), + mergeMap((activeGroup: Group) => { + if (activeGroup != null && activeGroup.uuid === group.uuid) { + return observableOf(true); + } + return observableOf(false); + })); + } + + /** + * Deletes given subgroup from the group currently being edited + * @param subgroup Group we want to delete from the subgroups of the group currently being edited + */ + deleteSubgroupFromGroup(subgroup: Group) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); + this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup); + this.forceUpdateGroups(activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); + } + + /** + * Adds given subgroup to the group currently being edited + * @param subgroup Subgroup to add to group currently being edited + */ + addSubgroupToGroup(subgroup: Group) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + if (activeGroup.uuid !== subgroup.uuid) { + const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup); + this.showNotifications('addSubgroup', response, subgroup.name, activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup')); + } + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); + this.forceUpdateGroups(this.groupBeingEdited); + } + + /** + * Search in the groups (searches by group name and by uuid exact match) + * @param data Contains query param + */ + search(data: any) { + const query: string = data.query; + if (query != null && this.currentSearchQuery !== query) { + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); + this.currentSearchQuery = query; + this.configSearch.currentPage = 1; + } + this.searchDone = true; + this.groupsSearch = this.groupDataService.searchGroups(this.currentSearchQuery, { + currentPage: this.configSearch.currentPage, + elementsPerPage: this.configSearch.pageSize + }); + } + + /** + * Force-update the list of groups by first clearing the cache of results of this active groups' subgroups, then performing a new REST call + * @param activeGroup Group currently being edited + */ + public forceUpdateGroups(activeGroup: Group) { + this.groupDataService.clearGroupLinkRequests(activeGroup._links.subgroups.href); + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup)); + this.subgroupsOfGroup = this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, { + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize + }); + } + + /** + * unsub all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } + + /** + * Shows a notification based on the success/failure of the request + * @param messageSuffix Suffix for message + * @param response RestResponse observable containing success/failure request + * @param nameObject Object request was about + * @param activeGroup Group currently being edited + */ + showNotifications(messageSuffix: string, response: Observable, nameObject: string, activeGroup: Group) { + response.pipe(take(1)).subscribe((restResponse: RestResponse) => { + if (restResponse.isSuccessful) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject })); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject })); + } + }) + } + + /** + * Reset all input-fields to be empty and search all search + */ + clearFormAndResetResult() { + this.searchForm.patchValue({ + query: '', + }); + this.search({ query: '' }); + } +} diff --git a/src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts b/src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts new file mode 100644 index 0000000000..3a0f3bc5c5 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts @@ -0,0 +1,49 @@ +import { Action } from '@ngrx/store'; +import { Group } from '../../../core/eperson/models/group.model'; +import { type } from '../../../shared/ngrx/type'; + +/** + * For each action type in an action group, make a simple + * enum object for all of this group's action types. + * + * The 'type' utility function coerces strings into string + * literal types and runs a simple check to guarantee all + * action types in the application are unique. + */ +export const GroupRegistryActionTypes = { + + EDIT_GROUP: type('dspace/epeople-registry/EDIT_GROUP'), + CANCEL_EDIT_GROUP: type('dspace/epeople-registry/CANCEL_EDIT_GROUP'), +}; + +/* tslint:disable:max-classes-per-file */ +/** + * Used to edit a Group in the Group registry + */ +export class GroupRegistryEditGroupAction implements Action { + type = GroupRegistryActionTypes.EDIT_GROUP; + + group: Group; + + constructor(group: Group) { + this.group = group; + } +} + +/** + * Used to cancel the editing of a Group in the Group registry + */ +export class GroupRegistryCancelGroupAction implements Action { + type = GroupRegistryActionTypes.CANCEL_EDIT_GROUP; +} + +/* tslint:enable:max-classes-per-file */ + +/** + * Export a type alias of all actions in this action group + * so that reducers can easily compose action types + * These are all the actions to perform on the EPeople registry state + */ +export type GroupRegistryAction + = GroupRegistryEditGroupAction + | GroupRegistryCancelGroupAction diff --git a/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.spec.ts b/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.spec.ts new file mode 100644 index 0000000000..6c9f9d327a --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.spec.ts @@ -0,0 +1,54 @@ +import { GroupMock } from '../../../shared/testing/group-mock'; +import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction } from './group-registry.actions'; +import { groupRegistryReducer, GroupRegistryState } from './group-registry.reducers'; + +const initialState: GroupRegistryState = { + editGroup: null, +}; + +const editState: GroupRegistryState = { + editGroup: GroupMock, +}; + +class NullAction extends GroupRegistryEditGroupAction { + type = null; + + constructor() { + super(undefined); + } +} + +describe('groupRegistryReducer', () => { + + it('should return the current state when no valid actions have been made', () => { + const state = initialState; + const action = new NullAction(); + const newState = groupRegistryReducer(state, action); + + expect(newState).toEqual(state); + }); + + it('should start with an initial state', () => { + const state = initialState; + const action = new NullAction(); + const initState = groupRegistryReducer(undefined, action); + + expect(initState).toEqual(state); + }); + + it('should update the current state to change the editGroup to a new group when GroupRegistryEditGroupAction is dispatched', () => { + const state = editState; + const action = new GroupRegistryEditGroupAction(GroupMock); + const newState = groupRegistryReducer(state, action); + + expect(newState.editGroup).toEqual(GroupMock); + }); + + it('should update the current state to remove the editGroup from the state when GroupRegistryCancelGroupAction is dispatched', () => { + const state = editState; + const action = new GroupRegistryCancelGroupAction(); + const newState = groupRegistryReducer(state, action); + + expect(newState.editGroup).toEqual(null); + }); +}); diff --git a/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts b/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts new file mode 100644 index 0000000000..eca6c282a7 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts @@ -0,0 +1,43 @@ +import { Group } from '../../../core/eperson/models/group.model'; +import { GroupRegistryAction, GroupRegistryActionTypes, GroupRegistryEditGroupAction } from './group-registry.actions'; + +/** + * The metadata registry state. + * @interface GroupRegistryState + */ +export interface GroupRegistryState { + editGroup: Group; +} + +/** + * The initial state. + */ +const initialState: GroupRegistryState = { + editGroup: null, +}; + +/** + * Reducer that handles GroupRegistryActions to modify Groups + * @param state The current GroupRegistryState + * @param action The GroupRegistryAction to perform on the state + */ +export function groupRegistryReducer(state = initialState, action: GroupRegistryAction): GroupRegistryState { + + switch (action.type) { + + case GroupRegistryActionTypes.EDIT_GROUP: { + return Object.assign({}, state, { + editGroup: (action as GroupRegistryEditGroupAction).group + }); + } + + case GroupRegistryActionTypes.CANCEL_EDIT_GROUP: { + return Object.assign({}, state, { + editGroup: null + }); + } + + default: + return state; + } +} diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html new file mode 100644 index 0000000000..3bd7d7ac4f --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html @@ -0,0 +1,84 @@ +
+
+
+ + + +
+ +
+ + +
+
+
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + +
{{messagePrefix + 'table.id' | translate}}{{messagePrefix + 'table.name' | translate}}{{messagePrefix + 'table.members' | translate}}{{messagePrefix + 'table.edit' | translate}}
{{group.id}}{{group.name}}{{(getMembers(group) | async)?.payload?.totalElements + (getSubgroups(group) | async)?.payload?.totalElements}} +
+ + +
+
+
+
+ + + +
+
+
diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts new file mode 100644 index 0000000000..7dff4f9bd1 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts @@ -0,0 +1,139 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule, By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { RemoteData } from '../../../core/data/remote-data'; +import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { EPerson } from '../../../core/eperson/models/eperson.model'; +import { Group } from '../../../core/eperson/models/group.model'; +import { RouteService } from '../../../core/services/route.service'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { MockRouter } from '../../../shared/mocks/mock-router'; +import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock'; +import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { routeServiceStub } from '../../../shared/testing/route-service-stub'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { GroupsRegistryComponent } from './groups-registry.component'; + +describe('GroupRegistryComponent', () => { + let component: GroupsRegistryComponent; + let fixture: ComponentFixture; + let ePersonDataServiceStub: any; + let groupsDataServiceStub: any; + + let mockGroups; + let mockEPeople; + + beforeEach(async(() => { + mockGroups = [GroupMock, GroupMock2]; + mockEPeople = [EPersonMock, EPersonMock2]; + ePersonDataServiceStub = { + findAllByHref(href: string): Observable>> { + switch (href) { + case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons': + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [])); + case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons': + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [EPersonMock])); + default: + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [])); + } + } + }; + groupsDataServiceStub = { + allGroups: mockGroups, + findAllByHref(href: string): Observable>> { + switch (href) { + case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups': + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [])); + case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups': + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [GroupMock2])); + default: + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [])); + } + }, + getGroupEditPageRouterLink(group: Group): string { + return '/admin/access-control/groups/' + group.id; + }, + getGroupRegistryRouterLink(): string { + return '/admin/access-control/groups'; + }, + searchGroups(query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allGroups)); + } + const result = this.allGroups.find((group: Group) => { + return (group.id.includes(query)) + }); + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result])); + } + }; + TestBed.configureTestingModule({ + imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + declarations: [GroupsRegistryComponent], + providers: [GroupsRegistryComponent, + { provide: EPersonDataService, useValue: ePersonDataServiceStub }, + { provide: GroupDataService, useValue: groupsDataServiceStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: new MockRouter() }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupsRegistryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create GroupRegistryComponent', inject([GroupsRegistryComponent], (comp: GroupsRegistryComponent) => { + expect(comp).toBeDefined(); + })); + + it('should display list of groups', () => { + const groupIdsFound = fixture.debugElement.queryAll(By.css('#groups tr td:first-child')); + expect(groupIdsFound.length).toEqual(2); + mockGroups.map((group: Group) => { + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === group.uuid); + })).toBeTruthy(); + }) + }); + + describe('search', () => { + describe('when searching with query', () => { + let groupIdsFound; + beforeEach(fakeAsync(() => { + component.search({ query: GroupMock2.id }); + tick(); + fixture.detectChanges(); + groupIdsFound = fixture.debugElement.queryAll(By.css('#groups tr td:first-child')); + })); + + it('should display search result', () => { + expect(groupIdsFound.length).toEqual(1); + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === GroupMock2.uuid); + })).toBeTruthy(); + }); + }); + }); +}); diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts new file mode 100644 index 0000000000..c8ab102d30 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts @@ -0,0 +1,154 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { RemoteData } from '../../../core/data/remote-data'; +import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { EPerson } from '../../../core/eperson/models/eperson.model'; +import { Group } from '../../../core/eperson/models/group.model'; +import { RouteService } from '../../../core/services/route.service'; +import { hasValue } from '../../../shared/empty.util'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; + +@Component({ + selector: 'ds-groups-registry', + templateUrl: './groups-registry.component.html', +}) +/** + * A component used for managing all existing groups within the repository. + * The admin can create, edit or delete groups here. + */ +export class GroupsRegistryComponent implements OnInit { + + messagePrefix = 'admin.access-control.groups.'; + + /** + * Pagination config used to display the list of groups + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'groups-list-pagination', + pageSize: 5, + currentPage: 1 + }); + + /** + * A list of all the current groups within the repository or the result of the search + */ + groups: Observable>>; + + // The search form + searchForm; + + // Current search in groups registry + currentSearchQuery: string; + + constructor(private groupService: GroupDataService, + private ePersonDataService: EPersonDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder, + protected routeService: RouteService, + private router: Router) { + this.currentSearchQuery = ''; + this.searchForm = this.formBuilder.group(({ + query: this.currentSearchQuery, + })); + } + + ngOnInit() { + this.search({ query: this.currentSearchQuery }); + } + + /** + * Event triggered when the user changes page + * @param event + */ + onPageChange(event) { + this.config.currentPage = event; + this.search({ query: this.currentSearchQuery }) + } + + /** + * Search in the groups (searches by group name and by uuid exact match) + * @param data Contains query param + */ + search(data: any) { + const query: string = data.query; + if (query != null && this.currentSearchQuery !== query) { + this.router.navigateByUrl(this.groupService.getGroupRegistryRouterLink()); + this.currentSearchQuery = query; + this.config.currentPage = 1; + } + this.groups = this.groupService.searchGroups(this.currentSearchQuery.trim(), { + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize + }); + } + + /** + * Delete Group + */ + deleteGroup(group: Group) { + // TODO (backend) + console.log('TODO implement editGroup', group); + this.notificationsService.error('TODO implement deleteGroup (not yet implemented in backend)'); + if (hasValue(group.id)) { + this.groupService.deleteGroup(group).pipe(take(1)).subscribe((success: boolean) => { + if (success) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name })); + this.forceUpdateGroup(); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + 'notification.deleted.failure', { name: group.name })); + } + }) + } + } + + /** + * Force-update the list of groups by first clearing the cache related to groups, then performing a new REST call + */ + public forceUpdateGroup() { + this.groupService.clearGroupsRequests(); + this.search({ query: this.currentSearchQuery }) + } + + /** + * Get the members (epersons embedded value of a group) + * @param group + */ + getMembers(group: Group): Observable>> { + return this.ePersonDataService.findAllByHref(group._links.epersons.href); + } + + /** + * Get the subgroups (groups embedded value of a group) + * @param group + */ + getSubgroups(group: Group): Observable>> { + return this.groupService.findAllByHref(group._links.subgroups.href); + } + + /** + * Reset all input-fields to be empty and search all search + */ + clearFormAndResetResult() { + this.searchForm.patchValue({ + query: '', + }); + this.search({ query: '' }); + } + + /** + * Extract optional UUID from a group name => To be resolved to community or collection with link + * (Or will be resolved in backend and added to group object, tbd) //TODO + * @param groupName + */ + getOptionalComColFromName(groupName: string): string { + return this.groupService.getUUIDFromString(groupName); + } +} diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index b4a68d692a..aa47c93102 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -1,9 +1,9 @@ -import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; -import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { RouterModule } from '@angular/router'; import { getAdminModulePath } from '../app-routing.module'; import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; const REGISTRIES_MODULE_PATH = 'registries'; const ACCESS_CONTROL_MODULE_PATH = 'access-control'; @@ -12,6 +12,10 @@ export function getRegistriesModulePath() { return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString(); } +export function getAccessControlModulePath() { + return new URLCombiner(getAdminModulePath(), ACCESS_CONTROL_MODULE_PATH).toString(); +} + @NgModule({ imports: [ RouterModule.forChild([ @@ -28,8 +32,8 @@ export function getRegistriesModulePath() { resolve: { breadcrumb: I18nBreadcrumbResolver }, component: AdminSearchPageComponent, data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } - }, - ]) + } + ]), ] }) export class AdminRoutingModule { diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index e3f55b8e18..79aad4599d 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -336,7 +336,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { model: { type: MenuItemType.LINK, text: 'menu.section.access_control_groups', - link: '' + link: '/admin/access-control/groups' } as LinkMenuItemModel, }, { diff --git a/src/app/+bitstream-page/bitstream-page-routing.module.ts b/src/app/+bitstream-page/bitstream-page-routing.module.ts new file mode 100644 index 0000000000..14d688064c --- /dev/null +++ b/src/app/+bitstream-page/bitstream-page-routing.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { BitstreamPageResolver } from './bitstream-page.resolver'; + +const EDIT_BITSTREAM_PATH = ':id/edit'; + +/** + * Routing module to help navigate Bitstream pages + */ +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: EDIT_BITSTREAM_PATH, + component: EditBitstreamPageComponent, + resolve: { + bitstream: BitstreamPageResolver + }, + canActivate: [AuthenticatedGuard] + } + ]) + ], + providers: [ + BitstreamPageResolver, + ] +}) +export class BitstreamPageRoutingModule { +} diff --git a/src/app/+bitstream-page/bitstream-page.module.ts b/src/app/+bitstream-page/bitstream-page.module.ts new file mode 100644 index 0000000000..24b4cd512f --- /dev/null +++ b/src/app/+bitstream-page/bitstream-page.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component'; +import { BitstreamPageRoutingModule } from './bitstream-page-routing.module'; + +/** + * This module handles all components that are necessary for Bitstream related pages + */ +@NgModule({ + imports: [ + CommonModule, + SharedModule, + BitstreamPageRoutingModule + ], + declarations: [ + EditBitstreamPageComponent + ] +}) +export class BitstreamPageModule { +} diff --git a/src/app/+bitstream-page/bitstream-page.resolver.ts b/src/app/+bitstream-page/bitstream-page.resolver.ts new file mode 100644 index 0000000000..8e9f64fcc1 --- /dev/null +++ b/src/app/+bitstream-page/bitstream-page.resolver.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { RemoteData } from '../core/data/remote-data'; +import { Observable } from 'rxjs/internal/Observable'; +import { find } from 'rxjs/operators'; +import { hasValue } from '../shared/empty.util'; +import { Bitstream } from '../core/shared/bitstream.model'; +import { BitstreamDataService } from '../core/data/bitstream-data.service'; + +/** + * This class represents a resolver that requests a specific bitstream before the route is activated + */ +@Injectable() +export class BitstreamPageResolver implements Resolve> { + constructor(private bitstreamService: BitstreamDataService) { + } + + /** + * Method for resolving a bitstream based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found bitstream based on the parameters in the current route, + * or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + return this.bitstreamService.findById(route.params.id) + .pipe( + find((RD) => hasValue(RD.error) || RD.hasSucceeded), + ); + } +} diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html new file mode 100644 index 0000000000..fd13e249a0 --- /dev/null +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html @@ -0,0 +1,29 @@ + +
+
+
+ +
+
+
+
+
+

{{bitstreamRD?.payload?.name}} ({{bitstreamRD?.payload?.sizeBytes | dsFileSize}})

+
+
+
+ +
+
+ + +
+
diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss new file mode 100644 index 0000000000..d212b5347c --- /dev/null +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss @@ -0,0 +1,8 @@ +:host { + ::ng-deep { + .switch { + position: absolute; + top: $spacer*2.5; + } + } +} diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts new file mode 100644 index 0000000000..c802622dc4 --- /dev/null +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -0,0 +1,216 @@ +import { EditBitstreamPageComponent } from './edit-bitstream-page.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { RemoteData } from '../../core/data/remote-data'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { ActivatedRoute } from '@angular/router'; +import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { BitstreamDataService } from '../../core/data/bitstream-data.service'; +import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; +import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; +import { Bitstream } from '../../core/shared/bitstream.model'; +import { NotificationType } from '../../shared/notifications/models/notification-type'; +import { INotification, Notification } from '../../shared/notifications/models/notification.model'; +import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; +import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level'; +import { hasValue } from '../../shared/empty.util'; +import { FormControl, FormGroup } from '@angular/forms'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { FileSizePipe } from '../../shared/utils/file-size-pipe'; +import { RestResponse } from '../../core/cache/response.models'; +import { VarDirective } from '../../shared/utils/var.directive'; + +const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); +const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); +const successNotification: INotification = new Notification('id', NotificationType.Success, 'success'); + +let notificationsService: NotificationsService; +let formService: DynamicFormService; +let bitstreamService: BitstreamDataService; +let bitstreamFormatService: BitstreamFormatDataService; +let bitstream: Bitstream; +let selectedFormat: BitstreamFormat; +let allFormats: BitstreamFormat[]; + +describe('EditBitstreamPageComponent', () => { + let comp: EditBitstreamPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + allFormats = [ + Object.assign({ + id: '1', + shortDescription: 'Unknown', + description: 'Unknown format', + supportLevel: BitstreamFormatSupportLevel.Unknown, + _links: { + self: { href: 'format-selflink-1' } + } + }), + Object.assign({ + id: '2', + shortDescription: 'PNG', + description: 'Portable Network Graphics', + supportLevel: BitstreamFormatSupportLevel.Known, + _links: { + self: { href: 'format-selflink-2' } + } + }), + Object.assign({ + id: '3', + shortDescription: 'GIF', + description: 'Graphics Interchange Format', + supportLevel: BitstreamFormatSupportLevel.Known, + _links: { + self: { href: 'format-selflink-3' } + } + }) + ] as BitstreamFormat[]; + selectedFormat = allFormats[1]; + notificationsService = jasmine.createSpyObj('notificationsService', + { + info: infoNotification, + warning: warningNotification, + success: successNotification + } + ); + formService = Object.assign({ + createFormGroup: (fModel: DynamicFormControlModel[]) => { + const controls = {}; + if (hasValue(fModel)) { + fModel.forEach((controlModel) => { + controls[controlModel.id] = new FormControl((controlModel as any).value); + }); + return new FormGroup(controls); + } + return undefined; + } + }); + bitstream = Object.assign(new Bitstream(), { + metadata: { + 'dc.description': [ + { + value: 'Bitstream description' + } + ], + 'dc.title': [ + { + value: 'Bitstream title' + } + ] + }, + format: observableOf(new RemoteData(false, false, true, null, selectedFormat)), + _links: { + self: 'bitstream-selflink' + } + }); + bitstreamService = jasmine.createSpyObj('bitstreamService', { + findById: observableOf(new RemoteData(false, false, true, null, bitstream)), + update: observableOf(new RemoteData(false, false, true, null, bitstream)), + updateFormat: observableOf(new RestResponse(true, 200, 'OK')), + commitUpdates: {}, + patch: {} + }); + bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { + findAll: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), allFormats))) + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule], + declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective], + providers: [ + { provide: NotificationsService, useValue: notificationsService }, + { provide: DynamicFormService, useValue: formService }, + { provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: new RemoteData(false, false, true, null, bitstream) }), snapshot: { queryParams: {} } } }, + { provide: BitstreamDataService, useValue: bitstreamService }, + { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, + ChangeDetectorRef + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditBitstreamPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('on startup', () => { + let rawForm; + + beforeEach(() => { + rawForm = comp.formGroup.getRawValue(); + }); + + it('should fill in the bitstream\'s title', () => { + expect(rawForm.fileNamePrimaryContainer.fileName).toEqual(bitstream.name); + }); + + it('should fill in the bitstream\'s description', () => { + expect(rawForm.descriptionContainer.description).toEqual(bitstream.firstMetadataValue('dc.description')); + }); + + it('should select the correct format', () => { + expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id); + }); + + it('should put the \"New Format\" input on invisible', () => { + expect(comp.formLayout.newFormat.grid.host).toContain('invisible'); + }); + }); + + describe('when an unknown format is selected', () => { + beforeEach(() => { + comp.updateNewFormatLayout(allFormats[0].id); + }); + + it('should remove the invisible class from the \"New Format\" input', () => { + expect(comp.formLayout.newFormat.grid.host).not.toContain('invisible'); + }); + }); + + describe('onSubmit', () => { + describe('when selected format hasn\'t changed', () => { + beforeEach(() => { + comp.onSubmit(); + }); + + it('should call update', () => { + expect(bitstreamService.update).toHaveBeenCalled(); + }); + + it('should commit the updates', () => { + expect(bitstreamService.commitUpdates).toHaveBeenCalled(); + }); + }); + + describe('when selected format has changed', () => { + beforeEach(() => { + comp.formGroup.patchValue({ + formatContainer: { + selectedFormat: allFormats[2].id + } + }); + fixture.detectChanges(); + comp.onSubmit(); + }); + + it('should call update', () => { + expect(bitstreamService.update).toHaveBeenCalled(); + }); + + it('should call updateFormat', () => { + expect(bitstreamService.updateFormat).toHaveBeenCalled(); + }); + + it('should commit the updates', () => { + expect(bitstreamService.commitUpdates).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts new file mode 100644 index 0000000000..cce6932cd1 --- /dev/null +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -0,0 +1,524 @@ +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { Bitstream } from '../../core/shared/bitstream.model'; +import { ActivatedRoute, Router } from '@angular/router'; +import { filter, map, switchMap } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicFormLayout, + DynamicFormService, + DynamicInputModel, + DynamicSelectModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; +import { FormGroup } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; +import { cloneDeep } from 'lodash'; +import { BitstreamDataService } from '../../core/data/bitstream-data.service'; +import { + getAllSucceededRemoteData, getAllSucceededRemoteDataPayload, + getFirstSucceededRemoteDataPayload, + getRemoteDataPayload, + getSucceededRemoteData +} from '../../core/shared/operators'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; +import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; +import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level'; +import { RestResponse } from '../../core/cache/response.models'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { Metadata } from '../../core/shared/metadata.utils'; +import { Location } from '@angular/common'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { getItemEditPath } from '../../+item-page/item-page-routing.module'; + +@Component({ + selector: 'ds-edit-bitstream-page', + styleUrls: ['./edit-bitstream-page.component.scss'], + templateUrl: './edit-bitstream-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * Page component for editing a bitstream + */ +export class EditBitstreamPageComponent implements OnInit, OnDestroy { + + /** + * The bitstream's remote data observable + * Tracks changes and updates the view + */ + bitstreamRD$: Observable>; + + /** + * The formats their remote data observable + * Tracks changes and updates the view + */ + bitstreamFormatsRD$: Observable>>; + + /** + * The bitstream to edit + */ + bitstream: Bitstream; + + /** + * The originally selected format + */ + originalFormat: BitstreamFormat; + + /** + * A list of all available bitstream formats + */ + formats: BitstreamFormat[]; + + /** + * @type {string} Key prefix used to generate form messages + */ + KEY_PREFIX = 'bitstream.edit.form.'; + + /** + * @type {string} Key suffix used to generate form labels + */ + LABEL_KEY_SUFFIX = '.label'; + + /** + * @type {string} Key suffix used to generate form labels + */ + HINT_KEY_SUFFIX = '.hint'; + + /** + * @type {string} Key prefix used to generate notification messages + */ + NOTIFICATIONS_PREFIX = 'bitstream.edit.notifications.'; + + /** + * Options for fetching all bitstream formats + */ + findAllOptions = { elementsPerPage: 9999 }; + + /** + * The Dynamic Input Model for the file's name + */ + fileNameModel = new DynamicInputModel({ + id: 'fileName', + name: 'fileName', + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'You must provide a file name for the bitstream' + } + }); + + /** + * The Dynamic Switch Model for the file's name + */ + primaryBitstreamModel = new DynamicCustomSwitchModel({ + id: 'primaryBitstream', + name: 'primaryBitstream' + }); + + /** + * The Dynamic TextArea Model for the file's description + */ + descriptionModel = new DynamicTextAreaModel({ + id: 'description', + name: 'description', + rows: 10 + }); + + /** + * The Dynamic Input Model for the file's embargo (disabled on this page) + */ + embargoModel = new DynamicInputModel({ + id: 'embargo', + name: 'embargo', + disabled: true + }); + + /** + * The Dynamic Input Model for the selected format + */ + selectedFormatModel = new DynamicSelectModel({ + id: 'selectedFormat', + name: 'selectedFormat' + }); + + /** + * The Dynamic Input Model for supplying more format information + */ + newFormatModel = new DynamicInputModel({ + id: 'newFormat', + name: 'newFormat' + }); + + /** + * All input models in a simple array for easier iterations + */ + inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.embargoModel, this.selectedFormatModel, this.newFormatModel]; + + /** + * The dynamic form fields used for editing the information of a bitstream + * @type {(DynamicInputModel | DynamicTextAreaModel)[]} + */ + formModel: DynamicFormControlModel[] = [ + new DynamicFormGroupModel({ + id: 'fileNamePrimaryContainer', + group: [ + this.fileNameModel, + this.primaryBitstreamModel + ] + }), + new DynamicFormGroupModel({ + id: 'descriptionContainer', + group: [ + this.descriptionModel + ] + }), + new DynamicFormGroupModel({ + id: 'embargoContainer', + group: [ + this.embargoModel + ] + }), + new DynamicFormGroupModel({ + id: 'formatContainer', + group: [ + this.selectedFormatModel, + this.newFormatModel + ] + }) + ]; + + /** + * The base layout of the "Other Format" input + */ + newFormatBaseLayout = 'col col-sm-6 d-inline-block'; + + /** + * Layout used for structuring the form inputs + */ + formLayout: DynamicFormLayout = { + fileName: { + grid: { + host: 'col col-sm-8 d-inline-block' + } + }, + primaryBitstream: { + grid: { + host: 'col col-sm-4 d-inline-block switch' + } + }, + description: { + grid: { + host: 'col-12 d-inline-block' + } + }, + embargo: { + grid: { + host: 'col-12 d-inline-block' + } + }, + selectedFormat: { + grid: { + host: 'col col-sm-6 d-inline-block' + } + }, + newFormat: { + grid: { + host: this.newFormatBaseLayout + ' invisible' + } + }, + fileNamePrimaryContainer: { + grid: { + host: 'row position-relative' + } + }, + descriptionContainer: { + grid: { + host: 'row' + } + }, + embargoContainer: { + grid: { + host: 'row' + } + }, + formatContainer: { + grid: { + host: 'row' + } + } + }; + + /** + * The form group of this form + */ + formGroup: FormGroup; + + /** + * The ID of the item the bitstream originates from + * Taken from the current query parameters when present + */ + itemId: string; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + protected subs: Subscription[] = []; + + constructor(private route: ActivatedRoute, + private router: Router, + private location: Location, + private formService: DynamicFormService, + private translate: TranslateService, + private bitstreamService: BitstreamDataService, + private notificationsService: NotificationsService, + private bitstreamFormatService: BitstreamFormatDataService) { + } + + /** + * Initialize the component + * - Create a FormGroup using the FormModel defined earlier + * - Subscribe on the route data to fetch the bitstream to edit and update the form values + * - Translate the form labels and hints + */ + ngOnInit(): void { + this.formGroup = this.formService.createFormGroup(this.formModel); + + this.itemId = this.route.snapshot.queryParams.itemId; + this.bitstreamRD$ = this.route.data.pipe(map((data) => data.bitstream)); + this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions); + + const bitstream$ = this.bitstreamRD$.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + switchMap((bitstream: Bitstream) => this.bitstreamService.findById(bitstream.id, followLink('format')).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + filter((bs: Bitstream) => hasValue(bs))) + ) + ); + + const allFormats$ = this.bitstreamFormatsRD$.pipe( + getSucceededRemoteData(), + getRemoteDataPayload() + ); + + this.subs.push( + observableCombineLatest( + bitstream$, + allFormats$ + ).subscribe(([bitstream, allFormats]) => { + this.bitstream = bitstream as Bitstream; + this.formats = allFormats.page; + this.updateFormatModel(); + this.updateForm(this.bitstream); + }) + ); + + this.updateFieldTranslations(); + + this.subs.push( + this.translate.onLangChange + .subscribe(() => { + this.updateFieldTranslations(); + }) + ); + }; + + /** + * Update the current form values with bitstream properties + * @param bitstream + */ + updateForm(bitstream: Bitstream) { + this.formGroup.patchValue({ + fileNamePrimaryContainer: { + fileName: bitstream.name, + primaryBitstream: false + }, + descriptionContainer: { + description: bitstream.firstMetadataValue('dc.description') + }, + formatContainer: { + newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined + } + }); + this.bitstream.format.pipe( + getAllSucceededRemoteDataPayload() + ).subscribe((format: BitstreamFormat) => { + this.originalFormat = format; + this.formGroup.patchValue({ + formatContainer: { + selectedFormat: format.id + } + }); + this.updateNewFormatLayout(format.id); + }); + } + + /** + * Create the list of unknown format IDs an add options to the selectedFormatModel + */ + updateFormatModel() { + this.selectedFormatModel.options = this.formats.map((format: BitstreamFormat) => + Object.assign({ + value: format.id, + label: this.isUnknownFormat(format.id) ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription + })); + } + + /** + * Update the layout of the "Other Format" input depending on the selected format + * @param selectedId + */ + updateNewFormatLayout(selectedId: string) { + if (this.isUnknownFormat(selectedId)) { + this.formLayout.newFormat.grid.host = this.newFormatBaseLayout; + } else { + this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible'; + } + } + + /** + * Is the provided format (id) part of the list of unknown formats? + * @param id + */ + isUnknownFormat(id: string): boolean { + const format = this.formats.find((f: BitstreamFormat) => f.id === id); + return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown; + } + + /** + * Used to update translations of labels and hints on init and on language change + */ + private updateFieldTranslations() { + this.inputModels.forEach( + (fieldModel: DynamicFormControlModel) => { + this.updateFieldTranslation(fieldModel); + } + ); + } + + /** + * Update the translations of a DynamicFormControlModel + * @param fieldModel + */ + private updateFieldTranslation(fieldModel) { + fieldModel.label = this.translate.instant(this.KEY_PREFIX + fieldModel.id + this.LABEL_KEY_SUFFIX); + if (fieldModel.id !== this.primaryBitstreamModel.id) { + fieldModel.hint = this.translate.instant(this.KEY_PREFIX + fieldModel.id + this.HINT_KEY_SUFFIX); + } + } + + /** + * Fired whenever the form receives an update and changes the layout of the "Other Format" input, depending on the selected format + * @param event + */ + onChange(event) { + const model = event.model; + if (model.id === this.selectedFormatModel.id) { + this.updateNewFormatLayout(model.value); + } + } + + /** + * Check for changes against the bitstream and send update requests to the REST API + */ + onSubmit() { + const updatedValues = this.formGroup.getRawValue(); + const updatedBitstream = this.formToBitstream(updatedValues); + const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat); + const isNewFormat = selectedFormat.id !== this.originalFormat.id; + + let bitstream$; + + if (isNewFormat) { + bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe( + switchMap((formatResponse: RestResponse) => { + if (hasValue(formatResponse) && !formatResponse.isSuccessful) { + this.notificationsService.error( + this.translate.instant(this.NOTIFICATIONS_PREFIX + 'error.format.title'), + formatResponse.statusText + ); + } else { + return this.bitstreamService.findById(this.bitstream.id).pipe( + getFirstSucceededRemoteDataPayload() + ); + } + }) + ); + } else { + bitstream$ = observableOf(this.bitstream); + } + + bitstream$.pipe( + switchMap(() => { + return this.bitstreamService.update(updatedBitstream).pipe( + getFirstSucceededRemoteDataPayload() + ); + }) + ).subscribe(() => { + this.bitstreamService.commitUpdates(); + this.notificationsService.success( + this.translate.instant(this.NOTIFICATIONS_PREFIX + 'saved.title'), + this.translate.instant(this.NOTIFICATIONS_PREFIX + 'saved.content') + ); + this.navigateToItemEditBitstreams(); + }); + } + + /** + * Parse form data to an updated bitstream object + * @param rawForm Raw form data + */ + formToBitstream(rawForm): Bitstream { + const updatedBitstream = cloneDeep(this.bitstream); + const newMetadata = updatedBitstream.metadata; + // TODO: Set bitstream to primary when supported + const primary = rawForm.fileNamePrimaryContainer.primaryBitstream; + Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName); + Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description); + if (isNotEmpty(rawForm.formatContainer.newFormat)) { + Metadata.setFirstValue(newMetadata, 'dc.format', rawForm.formatContainer.newFormat); + } + updatedBitstream.metadata = newMetadata; + return updatedBitstream; + } + + /** + * Cancel the form and return to the previous page + */ + onCancel() { + this.navigateToItemEditBitstreams(); + } + + /** + * When the item ID is present, navigate back to the item's edit bitstreams page, otherwise go back to the previous + * page the user came from + */ + navigateToItemEditBitstreams() { + if (hasValue(this.itemId)) { + this.router.navigate([getItemEditPath(this.itemId), 'bitstreams']); + } else { + this.location.back(); + } + } + + /** + * Unsubscribe from open subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + +} diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html index e69de29bb2..dc702ee61e 100644 --- a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html @@ -0,0 +1,6 @@ + + diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts new file mode 100644 index 0000000000..8384385572 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts @@ -0,0 +1,121 @@ +import { ComponentFixture, TestBed} from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { ActivatedRoute } from '@angular/router'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { RemoteData } from '../../../core/data/remote-data'; +import { CollectionRolesComponent } from './collection-roles.component'; +import { Collection } from '../../../core/shared/collection.model'; +import { SharedModule } from '../../../shared/shared.module'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { RequestService } from '../../../core/data/request.service'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('CollectionRolesComponent', () => { + + let fixture: ComponentFixture; + let comp: CollectionRolesComponent; + let de: DebugElement; + + beforeEach(() => { + + const route = { + parent: { + data: observableOf({ + dso: new RemoteData( + false, + false, + true, + undefined, + Object.assign(new Collection(), { + _links: { + 'irrelevant': { + href: 'irrelevant link', + }, + 'adminGroup': { + href: 'adminGroup link', + }, + 'submittersGroup': { + href: 'submittersGroup link', + }, + 'itemReadGroup': { + href: 'itemReadGroup link', + }, + 'bitstreamReadGroup': { + href: 'bitstreamReadGroup link', + }, + 'workflowGroups/test': { + href: 'test workflow group link', + }, + }, + }), + ), + }) + } + }; + + const requestService = { + hasByHrefObservable: () => observableOf(true), + }; + + const groupDataService = { + findByHref: () => observableOf(new RemoteData( + false, + false, + true, + undefined, + {}, + 200, + )), + }; + + TestBed.configureTestingModule({ + imports: [ + SharedModule, + RouterTestingModule.withRoutes([]), + TranslateModule.forRoot(), + ], + declarations: [ + CollectionRolesComponent, + ], + providers: [ + { provide: ActivatedRoute, useValue: route }, + { provide: RequestService, useValue: requestService }, + { provide: GroupDataService, useValue: groupDataService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(CollectionRolesComponent); + comp = fixture.componentInstance; + de = fixture.debugElement; + + fixture.detectChanges(); + }); + + it('should display a collection admin role component', () => { + expect(de.query(By.css('ds-comcol-role .collection-admin'))) + .toBeTruthy(); + }); + + it('should display a submitters role component', () => { + expect(de.query(By.css('ds-comcol-role .submitters'))) + .toBeTruthy(); + }); + + it('should display a default item read role component', () => { + expect(de.query(By.css('ds-comcol-role .item_read'))) + .toBeTruthy(); + }); + + it('should display a default bitstream read role component', () => { + expect(de.query(By.css('ds-comcol-role .bitstream_read'))) + .toBeTruthy(); + }); + + it('should display a test workflow role component', () => { + expect(de.query(By.css('ds-comcol-role .test'))) + .toBeTruthy(); + }); +}); diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts index 39f72fd2ce..45f2f37b9b 100644 --- a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts @@ -1,4 +1,11 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { first, map } from 'rxjs/operators'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Collection } from '../../../core/shared/collection.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { ComcolRole } from '../../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role'; /** * Component for managing a collection's roles @@ -7,6 +14,48 @@ import { Component } from '@angular/core'; selector: 'ds-collection-roles', templateUrl: './collection-roles.component.html', }) -export class CollectionRolesComponent { - /* TODO: Implement Collection Edit - Roles */ +export class CollectionRolesComponent implements OnInit { + + dsoRD$: Observable>; + + /** + * The collection to manage, as an observable. + */ + get collection$(): Observable { + return this.dsoRD$.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + ) + } + + /** + * The different roles for the collection, as an observable. + */ + getComcolRoles(): Observable { + return this.collection$.pipe( + map((collection) => + [ + ComcolRole.COLLECTION_ADMIN, + ComcolRole.SUBMITTERS, + ComcolRole.ITEM_READ, + ComcolRole.BITSTREAM_READ, + ...Object.keys(collection._links) + .filter((link) => link.startsWith('workflowGroups/')) + .map((link) => new ComcolRole(link.substr('workflowGroups/'.length), link)), + ] + ), + ); + } + + constructor( + protected route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.parent.data.pipe( + first(), + map((data) => data.dso), + ); + } } diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html index e69de29bb2..231645a6a5 100644 --- a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html +++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html @@ -0,0 +1,6 @@ + + diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts new file mode 100644 index 0000000000..4894046b10 --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts @@ -0,0 +1,89 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { ActivatedRoute } from '@angular/router'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { CommunityRolesComponent } from './community-roles.component'; +import { Community } from '../../../core/shared/community.model'; +import { By } from '@angular/platform-browser'; +import { RemoteData } from '../../../core/data/remote-data'; +import { RequestService } from '../../../core/data/request.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('CommunityRolesComponent', () => { + + let fixture: ComponentFixture; + let comp: CommunityRolesComponent; + let de: DebugElement; + + beforeEach(() => { + + const route = { + parent: { + data: observableOf({ + dso: new RemoteData( + false, + false, + true, + undefined, + Object.assign(new Community(), { + _links: { + irrelevant: { + href: 'irrelevant link', + }, + adminGroup: { + href: 'adminGroup link', + }, + }, + }), + ), + }) + } + }; + + const requestService = { + hasByHrefObservable: () => observableOf(true), + }; + + const groupDataService = { + findByHref: () => observableOf(new RemoteData( + false, + false, + true, + undefined, + {}, + 200, + )), + }; + + TestBed.configureTestingModule({ + imports: [ + SharedModule, + RouterTestingModule.withRoutes([]), + TranslateModule.forRoot(), + ], + declarations: [ + CommunityRolesComponent, + ], + providers: [ + { provide: ActivatedRoute, useValue: route }, + { provide: RequestService, useValue: requestService }, + { provide: GroupDataService, useValue: groupDataService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(CommunityRolesComponent); + comp = fixture.componentInstance; + de = fixture.debugElement; + + fixture.detectChanges(); + }); + + it('should display a community admin role component', () => { + expect(de.query(By.css('ds-comcol-role .community-admin'))) + .toBeTruthy(); + }); +}); diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts index afa1fe14d1..336a56a584 100644 --- a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts +++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts @@ -1,4 +1,11 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { first, map } from 'rxjs/operators'; +import { Community } from '../../../core/shared/community.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { ComcolRole } from '../../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role'; +import { RemoteData } from '../../../core/data/remote-data'; /** * Component for managing a community's roles @@ -7,6 +14,38 @@ import { Component } from '@angular/core'; selector: 'ds-community-roles', templateUrl: './community-roles.component.html', }) -export class CommunityRolesComponent { - /* TODO: Implement Community Edit - Roles */ +export class CommunityRolesComponent implements OnInit { + + dsoRD$: Observable>; + + /** + * The community to manage, as an observable. + */ + get community$(): Observable { + return this.dsoRD$.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + ) + } + + /** + * The different roles for the community. + */ + getComcolRoles(): ComcolRole[] { + return [ + ComcolRole.COMMUNITY_ADMIN, + ]; + } + + constructor( + protected route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.parent.data.pipe( + first(), + map((data) => data.dso), + ); + } } diff --git a/src/app/+item-page/bitstreams/upload/upload-bitstream.component.html b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.html new file mode 100644 index 0000000000..289ede209a --- /dev/null +++ b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.html @@ -0,0 +1,41 @@ +
+ +
+
+

{{'item.bitstreams.upload.title' | translate}}

+ +
+ {{'item.bitstreams.upload.item' | translate}} + {{item.name}} +
+
+
+
+ + + + + + +
+
+
+
diff --git a/src/app/+item-page/bitstreams/upload/upload-bitstream.component.spec.ts b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.spec.ts new file mode 100644 index 0000000000..15a13884f3 --- /dev/null +++ b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.spec.ts @@ -0,0 +1,236 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { UploadBitstreamComponent } from './upload-bitstream.component'; +import { AuthService } from '../../../core/auth/auth.service'; +import { AuthServiceStub } from '../../../shared/testing/auth-service-stub'; +import { Item } from '../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs'; +import { + createPaginatedList, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../shared/testing/utils'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { RequestService } from '../../../core/data/request.service'; + +describe('UploadBistreamComponent', () => { + let comp: UploadBitstreamComponent; + let fixture: ComponentFixture; + + const bundle = Object.assign(new Bundle(), { + id: 'bundle', + uuid: 'bundle', + metadata: { + 'dc.title': [ + { + value: 'bundleName', + language: null + } + ] + }, + _links: { + self: { href: 'bundle-selflink' } + } + }); + const customName = 'Custom Name'; + const createdBundle = Object.assign(new Bundle(), { + id: 'created-bundle', + uuid: 'created-bundle', + metadata: { + 'dc.title': [ + { + value: customName, + language: null + } + ] + }, + _links: { + self: { href: 'created-bundle-selflink' } + } + }); + const itemName = 'fake-name'; + const mockItem = Object.assign(new Item(), { + id: 'fake-id', + handle: 'fake/handle', + metadata: { + 'dc.title': [ + { + language: null, + value: itemName + } + ] + }, + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([bundle])) + }); + let routeStub; + const routerStub = new RouterStub(); + const restEndpoint = 'fake-rest-endpoint'; + const mockItemDataService = jasmine.createSpyObj('mockItemDataService', { + getBitstreamsEndpoint: observableOf(restEndpoint), + createBundle: createSuccessfulRemoteDataObject$(createdBundle) + }); + const bundleService = jasmine.createSpyObj('bundleService', { + getBitstreamsEndpoint: observableOf(restEndpoint), + findById: createSuccessfulRemoteDataObject$(bundle) + }); + const authToken = 'fake-auth-token'; + const authServiceStub = Object.assign(new AuthServiceStub(), { + buildAuthHeader: () => authToken + }); + const notificationsServiceStub = new NotificationsServiceStub(); + const uploaderComponent = jasmine.createSpyObj('uploaderComponent', ['ngOnInit', 'ngAfterViewInit']); + const requestService = jasmine.createSpyObj('requestService', { + removeByHrefSubstring: {} + }); + + describe('when a file is uploaded', () => { + beforeEach(async(() => { + createUploadBitstreamTestingModule({}); + })); + + beforeEach(() => { + loadFixtureAndComp(); + }); + + describe('and it fails, calling onUploadError', () => { + beforeEach(() => { + comp.onUploadError(); + }); + + it('should display an error notification', () => { + expect(notificationsServiceStub.error).toHaveBeenCalled(); + }); + }); + + describe('and it succeeds, calling onCompleteItem', () => { + const createdBitstream = Object.assign(new Bitstream(), { + id: 'fake-bitstream' + }); + + beforeEach(() => { + comp.onCompleteItem(createdBitstream); + }); + + it('should navigate the user to the next page', () => { + expect(routerStub.navigate).toHaveBeenCalled(); + }); + }); + }); + + describe('when a bundle url parameter is present', () => { + beforeEach(async(() => { + createUploadBitstreamTestingModule({ + bundle: bundle.id + }); + })); + + beforeEach(() => { + loadFixtureAndComp(); + }); + + it('should set the selected id to the bundle\'s id', () => { + expect(comp.selectedBundleId).toEqual(bundle.id); + }); + + it('should set the selected name to the bundle\'s name', () => { + expect(comp.selectedBundleName).toEqual(bundle.name); + }); + + describe('and bundle name changed', () => { + beforeEach(() => { + comp.bundleNameChange(); + }); + + it('should clear out the selected id', () => { + expect(comp.selectedBundleId).toBeUndefined(); + }); + }); + }); + + describe('when a name is filled in, but no ID is selected', () => { + beforeEach(async(() => { + createUploadBitstreamTestingModule({}); + })); + + beforeEach(() => { + loadFixtureAndComp(); + comp.selectedBundleName = customName; + }); + + describe('createBundle', () => { + beforeEach(() => { + comp.createBundle(); + }); + + it('should create a new bundle', () => { + expect(mockItemDataService.createBundle).toHaveBeenCalledWith(mockItem.id, customName); + }); + + it('should set the selected id to the id of the new bundle', () => { + expect(comp.selectedBundleId).toEqual(createdBundle.id); + }); + + it('should display a success notification', () => { + expect(notificationsServiceStub.success).toHaveBeenCalled(); + }); + }); + }); + + /** + * Setup an UploadBitstreamComponent testing module with custom queryParams for the route + * @param queryParams + */ + function createUploadBitstreamTestingModule(queryParams) { + routeStub = { + data: observableOf({ + item: createSuccessfulRemoteDataObject(mockItem) + }), + queryParams: observableOf(queryParams), + snapshot: { + queryParams: queryParams, + params: { + id: mockItem.id + } + } + }; + + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], + declarations: [UploadBitstreamComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: routeStub }, + { provide: Router, useValue: routerStub }, + { provide: ItemDataService, useValue: mockItemDataService }, + { provide: NotificationsService, useValue: notificationsServiceStub }, + { provide: AuthService, useValue: authServiceStub }, + { provide: BundleDataService, useValue: bundleService }, + { provide: RequestService, useValue: requestService } + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + } + + /** + * Load the TestBed's fixture and component + */ + function loadFixtureAndComp() { + fixture = TestBed.createComponent(UploadBitstreamComponent); + comp = fixture.componentInstance; + comp.uploaderComponent = uploaderComponent; + fixture.detectChanges(); + } + +}); diff --git a/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts new file mode 100644 index 0000000000..536fc0931f --- /dev/null +++ b/src/app/+item-page/bitstreams/upload/upload-bitstream.component.ts @@ -0,0 +1,218 @@ +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { map, switchMap, take } from 'rxjs/operators'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UploaderOptions } from '../../../shared/uploader/uploader-options.model'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { AuthService } from '../../../core/auth/auth.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { getBitstreamModulePath } from '../../../app-routing.module'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { + getFirstSucceededRemoteDataPayload +} from '../../../core/shared/operators'; +import { UploaderComponent } from '../../../shared/uploader/uploader.component'; +import { getItemEditPath } from '../../item-page-routing.module'; +import { RequestService } from '../../../core/data/request.service'; + +@Component({ + selector: 'ds-upload-bitstream', + templateUrl: './upload-bitstream.component.html' +}) +/** + * Page component for uploading a bitstream to an item + */ +export class UploadBitstreamComponent implements OnInit, OnDestroy { + /** + * The file uploader component + */ + @ViewChild(UploaderComponent, {static: false}) uploaderComponent: UploaderComponent; + + /** + * The ID of the item to upload a bitstream to + */ + itemId: string; + + /** + * The item to upload a bitstream to + */ + itemRD$: Observable>; + + /** + * The item's bundles + */ + bundlesRD$: Observable>>; + + /** + * The ID of the currently selected bundle to upload a bitstream to + */ + selectedBundleId: string; + + /** + * The name of the currently selected bundle to upload a bitstream to + */ + selectedBundleName: string; + + /** + * The uploader configuration options + * @type {UploaderOptions} + */ + uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), { + // URL needs to contain something to not produce any errors. This will be replaced once a bundle has been selected. + url: 'placeholder', + authToken: null, + disableMultipart: false, + itemAlias: null + }); + + /** + * The prefix for all i18n notification messages within this component + */ + NOTIFICATIONS_PREFIX = 'item.bitstreams.upload.notifications.'; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + subs: Subscription[] = []; + + constructor(protected route: ActivatedRoute, + protected router: Router, + protected itemService: ItemDataService, + protected bundleService: BundleDataService, + protected authService: AuthService, + protected notificationsService: NotificationsService, + protected translate: TranslateService, + protected requestService: RequestService) { + } + + /** + * Initialize component properties: + * itemRD$ Fetched from the current route data (populated by BitstreamPageResolver) + * bundlesRD$ List of bundles on the item + * selectedBundleId Starts off by checking if the route's queryParams contain a "bundle" parameter. If none is found, + * the ID of the first bundle in the list is selected. + * Calls setUploadUrl after setting the selected bundle + */ + ngOnInit(): void { + this.itemId = this.route.snapshot.params.id; + this.itemRD$ = this.route.data.pipe(map((data) => data.item)); + this.bundlesRD$ = this.itemRD$.pipe( + switchMap((itemRD: RemoteData) => itemRD.payload.bundles) + ); + this.selectedBundleId = this.route.snapshot.queryParams.bundle; + if (isNotEmpty(this.selectedBundleId)) { + this.bundleService.findById(this.selectedBundleId).pipe( + getFirstSucceededRemoteDataPayload() + ).subscribe((bundle: Bundle) => { + this.selectedBundleName = bundle.name; + }); + this.setUploadUrl(); + } + } + + /** + * Create a new bundle with the filled in name on the current item + */ + createBundle() { + this.itemService.createBundle(this.itemId, this.selectedBundleName).pipe( + getFirstSucceededRemoteDataPayload() + ).subscribe((bundle: Bundle) => { + this.selectedBundleId = bundle.id; + this.notificationsService.success( + this.translate.instant(this.NOTIFICATIONS_PREFIX + 'bundle.created.title'), + this.translate.instant(this.NOTIFICATIONS_PREFIX + 'bundle.created.content') + ); + this.setUploadUrl(); + }); + } + + /** + * The user changed the bundle name + * Reset the bundle ID + */ + bundleNameChange() { + this.selectedBundleId = undefined; + } + + /** + * Set the upload url to match the selected bundle ID + */ + setUploadUrl() { + this.bundleService.getBitstreamsEndpoint(this.selectedBundleId).pipe(take(1)).subscribe((href: string) => { + this.uploadFilesOptions.url = href; + if (isEmpty(this.uploadFilesOptions.authToken)) { + this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); + } + // Re-initialize the uploader component to ensure the latest changes to the options are applied + if (this.uploaderComponent) { + this.uploaderComponent.ngOnInit(); + this.uploaderComponent.ngAfterViewInit(); + } + }); + } + + /** + * The request was successful, redirect the user to the new bitstream's edit page + * @param bitstream + */ + public onCompleteItem(bitstream) { + // Clear cached requests for this bundle's bitstreams to ensure lists on all pages are up-to-date + this.bundleService.getBitstreamsEndpoint(this.selectedBundleId).pipe(take(1)).subscribe((href: string) => { + this.requestService.removeByHrefSubstring(href); + }); + + // Bring over the item ID as a query parameter + const queryParams = { itemId: this.itemId }; + this.router.navigate([getBitstreamModulePath(), bitstream.id, 'edit'], { queryParams: queryParams }); + } + + /** + * The request was unsuccessful, display an error notification + */ + public onUploadError() { + this.notificationsService.error(null, this.translate.get(this.NOTIFICATIONS_PREFIX + 'upload.failed')); + } + + /** + * The user selected a bundle from the input suggestions + * Set the bundle ID and Name properties, as well as the upload URL + * @param bundle + */ + onClick(bundle: Bundle) { + this.selectedBundleId = bundle.id; + this.selectedBundleName = bundle.name; + this.setUploadUrl(); + } + + /** + * When cancel is clicked, navigate back to the item's edit bitstreams page + */ + onCancel() { + this.router.navigate([getItemEditPath(this.itemId), 'bitstreams']); + } + + /** + * @returns {string} the current URL + */ + getCurrentUrl() { + return this.router.url; + } + + /** + * Unsubscribe from all open subscriptions when the component is destroyed + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + +} diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 6900363253..75377d45b3 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -9,13 +9,13 @@ import { NotificationsService } from '../../../shared/notifications/notification import { TranslateService } from '@ngx-translate/core'; import { first, map } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; +import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; import { environment } from '../../../../environments/environment'; -@Injectable() /** * Abstract component for managing object updates of an item */ -export abstract class AbstractItemUpdateComponent implements OnInit { +export class AbstractItemUpdateComponent extends AbstractTrackableComponent implements OnInit { /** * The item to display the edit page for */ @@ -25,19 +25,6 @@ export abstract class AbstractItemUpdateComponent implements OnInit { * Should be initialized in the initializeUpdates method of the child component */ updates$: Observable; - /** - * The current url of this page - */ - url: string; - /** - * Prefix for this component's notification translate keys - * Should be initialized in the initializeNotificationsPrefix method of the child component - */ - notificationsPrefix; - /** - * The time span for being able to undo discarding changes - */ - discardTimeOut: number; constructor( protected itemService: ItemDataService, @@ -47,7 +34,7 @@ export abstract class AbstractItemUpdateComponent implements OnInit { protected translateService: TranslateService, protected route: ActivatedRoute ) { - + super(objectUpdatesService, notificationsService, translateService) } /** @@ -60,6 +47,7 @@ export abstract class AbstractItemUpdateComponent implements OnInit { map((data: RemoteData) => data.payload) ).subscribe((item: Item) => { this.item = item; + this.postItemInit(); }); this.discardTimeOut = environment.item.edit.undoTimeout; @@ -80,19 +68,44 @@ export abstract class AbstractItemUpdateComponent implements OnInit { } /** - * Initialize the values and updates of the current item's fields + * Actions to perform after the item has been initialized + * Abstract method: Should be overwritten in the sub class */ - abstract initializeUpdates(): void; + postItemInit(): void { + // Overwrite in subclasses + } + + /** + * Initialize the values and updates of the current item's fields + * Abstract method: Should be overwritten in the sub class + */ + initializeUpdates(): void { + // Overwrite in subclasses + } /** * Initialize the prefix for notification messages + * Abstract method: Should be overwritten in the sub class */ - abstract initializeNotificationsPrefix(): void; + initializeNotificationsPrefix(): void { + // Overwrite in subclasses + } /** * Sends all initial values of this item to the object updates service + * Abstract method: Should be overwritten in the sub class */ - abstract initializeOriginalFields(): void; + initializeOriginalFields(): void { + // Overwrite in subclasses + } + + /** + * Submit the current changes + * Abstract method: Should be overwritten in the sub class + */ + submit(): void { + // Overwrite in subclasses + } /** * Prevent unnecessary rerendering so fields don't lose focus @@ -101,13 +114,6 @@ export abstract class AbstractItemUpdateComponent implements OnInit { return update && update.field ? update.field.uuid : undefined; } - /** - * Checks whether or not there are currently updates for this item - */ - hasChanges(): Observable { - return this.objectUpdatesService.hasUpdates(this.url); - } - /** * Check if the current page is entirely valid */ @@ -130,49 +136,4 @@ export abstract class AbstractItemUpdateComponent implements OnInit { } ); } - - /** - * Submit the current changes - */ - abstract submit(): void; - - /** - * Request the object updates service to discard all current changes to this item - * Shows a notification to remind the user that they can undo this - */ - discard() { - const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut }); - this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); - } - - /** - * Request the object updates service to undo discarding all changes to this item - */ - reinstate() { - this.objectUpdatesService.reinstateFieldUpdates(this.url); - } - - /** - * Checks whether or not the item is currently reinstatable - */ - isReinstatable(): Observable { - return this.objectUpdatesService.isReinstatable(this.url); - } - - /** - * Get translated notification title - * @param key - */ - protected getNotificationTitle(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.title'); - } - - /** - * Get translated notification content - * @param key - */ - protected getNotificationContent(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.content'); - - } } diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index 2cbd0c57d1..d02aafcfa1 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -15,12 +15,19 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component'; import { ItemMetadataComponent } from './item-metadata/item-metadata.component'; import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; +import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.component'; import { SearchPageModule } from '../../+search-page/search-page.module'; import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component'; import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component'; +import { AbstractItemUpdateComponent } from './abstract-item-update/abstract-item-update.component'; import { ItemMoveComponent } from './item-move/item-move.component'; +import { ItemEditBitstreamBundleComponent } from './item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component'; +import { BundleDataService } from '../../core/data/bundle-data.service'; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { ItemEditBitstreamDragHandleComponent } from './item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component'; +import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component'; import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; @@ -32,12 +39,14 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version CommonModule, SharedModule, EditItemPageRoutingModule, - SearchPageModule + SearchPageModule, + DragDropModule ], declarations: [ EditItemPageComponent, ItemOperationComponent, AbstractSimpleItemActionComponent, + AbstractItemUpdateComponent, ModifyItemOverviewComponent, ItemWithdrawComponent, ItemReinstateComponent, @@ -50,11 +59,19 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version ItemBitstreamsComponent, ItemVersionHistoryComponent, EditInPlaceFieldComponent, + ItemEditBitstreamComponent, + ItemEditBitstreamBundleComponent, + PaginatedDragAndDropBitstreamListComponent, + EditInPlaceFieldComponent, EditRelationshipComponent, EditRelationshipListComponent, ItemCollectionMapperComponent, ItemMoveComponent, + ItemEditBitstreamDragHandleComponent, VirtualMetadataComponent, + ], + providers: [ + BundleDataService ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index b80e6e0678..dc017a9f92 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -1,3 +1,68 @@ -
+
+
+ + + + +
+
+
+
+ + {{'item.edit.bitstreams.headers.name' | translate}} +
+
{{'item.edit.bitstreams.headers.description' | translate}}
+
{{'item.edit.bitstreams.headers.format' | translate}}
+
{{'item.edit.bitstreams.headers.actions' | translate}}
+
+ + +
+ + + +
+
+ + + +
+
diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index e69de29bb2..0400e765de 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -0,0 +1,42 @@ +.header-row { + color: $table-dark-color; + background-color: $table-dark-bg; + border-color: $table-dark-border-color; +} + +.bundle-row { + color: $table-head-color; + background-color: $table-head-bg; + border-color: $table-border-color; +} + +.row-element { + padding: 12px; + padding: 0.75em; + border-bottom: $table-border-width solid $table-border-color; +} + +.drag-handle { + visibility: hidden; + &:hover { + cursor: grab; + } +} + +:host ::ng-deep .bitstream-row:hover .drag-handle { + visibility: visible !important; +} + +.cdk-drag-preview { + margin-left: 0; + box-sizing: border-box; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts new file mode 100644 index 0000000000..9184889257 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -0,0 +1,224 @@ +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { Item } from '../../../core/shared/item.model'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ItemBitstreamsComponent } from './item-bitstreams.component'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { GLOBAL_CONFIG } from '../../../../config'; +import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; +import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; +import { NotificationType } from '../../../shared/notifications/models/notification-type'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { getMockRequestService } from '../../../shared/mocks/mock-request.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { RequestService } from '../../../core/data/request.service'; +import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { RestResponse } from '../../../core/cache/response.models'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; + +let comp: ItemBitstreamsComponent; +let fixture: ComponentFixture; + +const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); +const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); +const successNotification: INotification = new Notification('id', NotificationType.Success, 'success'); +const bitstream1 = Object.assign(new Bitstream(), { + id: 'bitstream1', + uuid: 'bitstream1' +}); +const bitstream2 = Object.assign(new Bitstream(), { + id: 'bitstream2', + uuid: 'bitstream2' +}); +const fieldUpdate1 = { + field: bitstream1, + changeType: undefined +}; +const fieldUpdate2 = { + field: bitstream2, + changeType: FieldChangeType.REMOVE +}; +const bundle = Object.assign(new Bundle(), { + id: 'bundle1', + uuid: 'bundle1', + _links: { + self: { href: 'bundle1-selflink' } + }, + bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2]) +}); +const moveOperations = [ + { + op: 'move', + from: '/0', + path: '/1' + } +]; +const date = new Date(); +const url = 'thisUrl'; +let item: Item; +let itemService: ItemDataService; +let objectUpdatesService: ObjectUpdatesService; +let router: any; +let route: ActivatedRoute; +let notificationsService: NotificationsService; +let bitstreamService: BitstreamDataService; +let objectCache: ObjectCacheService; +let requestService: RequestService; +let searchConfig: SearchConfigurationService; +let bundleService: BundleDataService; + +describe('ItemBitstreamsComponent', () => { + beforeEach(async(() => { + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', + { + getFieldUpdates: observableOf({ + [bitstream1.uuid]: fieldUpdate1, + [bitstream2.uuid]: fieldUpdate2, + }), + getFieldUpdatesExclusive: observableOf({ + [bitstream1.uuid]: fieldUpdate1, + [bitstream2.uuid]: fieldUpdate2, + }), + saveAddFieldUpdate: {}, + discardFieldUpdates: {}, + discardAllFieldUpdates: {}, + reinstateFieldUpdates: observableOf(true), + initialize: {}, + getUpdatedFields: observableOf([bitstream1, bitstream2]), + getLastModified: observableOf(date), + hasUpdates: observableOf(true), + isReinstatable: observableOf(false), + isValidPage: observableOf(true), + getMoveOperations: observableOf(moveOperations) + } + ); + router = Object.assign(new RouterStub(), { + url: url + }); + notificationsService = jasmine.createSpyObj('notificationsService', + { + info: infoNotification, + warning: warningNotification, + success: successNotification + } + ); + bitstreamService = jasmine.createSpyObj('bitstreamService', { + deleteAndReturnResponse: jasmine.createSpy('deleteAndReturnResponse') + }); + objectCache = jasmine.createSpyObj('objectCache', { + remove: jasmine.createSpy('remove') + }); + requestService = getMockRequestService(); + searchConfig = Object.assign( { + paginatedSearchOptions: observableOf({}) + }); + + item = Object.assign(new Item(), { + uuid: 'item', + id: 'item', + _links: { + self: { href: 'item-selflink' } + }, + bundles: createMockRDPaginatedObs([bundle]), + lastModified: date + }); + itemService = Object.assign( { + getBitstreams: () => createMockRDPaginatedObs([bitstream1, bitstream2]), + findById: () => createMockRDObs(item), + getBundles: () => createMockRDPaginatedObs([bundle]) + }); + route = Object.assign({ + parent: { + data: observableOf({ item: createMockRD(item) }) + }, + url: url + }); + bundleService = jasmine.createSpyObj('bundleService', { + patch: observableOf(new RestResponse(true, 200, 'OK')) + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ItemBitstreamsComponent, ObjectValuesPipe, VarDirective], + providers: [ + { provide: ItemDataService, useValue: itemService }, + { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: route }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: GLOBAL_CONFIG, useValue: { item: { edit: { undoTimeout: 10 } } } as any }, + { provide: BitstreamDataService, useValue: bitstreamService }, + { provide: ObjectCacheService, useValue: objectCache }, + { provide: RequestService, useValue: requestService }, + { provide: SearchConfigurationService, useValue: searchConfig }, + { provide: BundleDataService, useValue: bundleService }, + ChangeDetectorRef + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemBitstreamsComponent); + comp = fixture.componentInstance; + comp.url = url; + fixture.detectChanges(); + }); + + describe('when submit is called', () => { + beforeEach(() => { + comp.submit(); + }); + + it('should call deleteAndReturnResponse on the bitstreamService for the marked field', () => { + expect(bitstreamService.deleteAndReturnResponse).toHaveBeenCalledWith(bitstream2.id); + }); + + it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => { + expect(bitstreamService.deleteAndReturnResponse).not.toHaveBeenCalledWith(bitstream1.id); + }); + + it('should send out a patch for the move operations', () => { + expect(bundleService.patch).toHaveBeenCalled(); + }); + }); + + describe('discard', () => { + it('should discard ALL field updates', () => { + comp.discard(); + expect(objectUpdatesService.discardAllFieldUpdates).toHaveBeenCalled(); + }); + }); + + describe('reinstate', () => { + it('should reinstate field updates on the bundle', () => { + comp.reinstate(); + expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(bundle.self); + }); + }); +}); + +export function createMockRDPaginatedObs(list: any[]) { + return createMockRDObs(new PaginatedList(new PageInfo(), list)); +} + +export function createMockRDObs(obj: any) { + return observableOf(createMockRD(obj)); +} + +export function createMockRD(obj: any) { + return new RemoteData(false, false, true, null, obj); +} diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 71f25cd5cf..bdb1ec23a5 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -1,4 +1,34 @@ -import { Component } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core'; +import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; +import { filter, map, switchMap, take, tap } from 'rxjs/operators'; +import { Observable } from 'rxjs/internal/Observable'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; +import { zip as observableZip, combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; +import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { RequestService } from '../../../core/data/request.service'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { Item } from '../../../core/shared/item.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; +import { Operation } from 'fast-json-patch'; +import { MoveOperation } from 'fast-json-patch/lib/core'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; +import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; @Component({ selector: 'ds-item-bitstreams', @@ -8,6 +38,273 @@ import { Component } from '@angular/core'; /** * Component for displaying an item's bitstreams edit page */ -export class ItemBitstreamsComponent { - /* TODO implement */ +export class ItemBitstreamsComponent extends AbstractItemUpdateComponent implements OnDestroy { + + /** + * The currently listed bundles + */ + bundles$: Observable; + + /** + * The page options to use for fetching the bundles + */ + bundlesOptions = { + id: 'bundles-pagination-options', + currentPage: 1, + pageSize: 9999 + } as any; + + /** + * The bootstrap sizes used for the columns within this table + */ + columnSizes = new ResponsiveTableSizes([ + // Name column + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + // Description column + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + // Format column + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + // Actions column + new ResponsiveColumnSizes(6, 5, 4, 3, 3) + ]); + + /** + * Are we currently submitting the changes? + * Used to disable any action buttons until the submit finishes + */ + submitting = false; + + /** + * A subscription that checks when the item is deleted in cache and reloads the item by sending a new request + * This is used to update the item in cache after bitstreams are deleted + */ + itemUpdateSubscription: Subscription; + + constructor( + public itemService: ItemDataService, + public objectUpdatesService: ObjectUpdatesService, + public router: Router, + public notificationsService: NotificationsService, + public translateService: TranslateService, + @Inject(GLOBAL_CONFIG) public EnvConfig: GlobalConfig, + public route: ActivatedRoute, + public bitstreamService: BitstreamDataService, + public objectCache: ObjectCacheService, + public requestService: RequestService, + public cdRef: ChangeDetectorRef, + public bundleService: BundleDataService + ) { + super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route); + } + + /** + * Set up and initialize all fields + */ + ngOnInit(): void { + super.ngOnInit(); + this.initializeItemUpdate(); + } + + /** + * Actions to perform after the item has been initialized + */ + postItemInit(): void { + this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: this.bundlesOptions})).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((bundlePage: PaginatedList) => bundlePage.page) + ); + } + + /** + * Initialize the notification messages prefix + */ + initializeNotificationsPrefix(): void { + this.notificationsPrefix = 'item.edit.bitstreams.notifications.'; + } + + /** + * Update the item (and view) when it's removed in the request cache + * Also re-initialize the original fields and updates + */ + initializeItemUpdate(): void { + this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe( + filter((exists: boolean) => !exists), + switchMap(() => this.itemService.findById(this.item.uuid)), + getSucceededRemoteData(), + ).subscribe((itemRD: RemoteData) => { + if (hasValue(itemRD)) { + this.item = itemRD.payload; + this.postItemInit(); + this.initializeOriginalFields(); + this.initializeUpdates(); + this.cdRef.detectChanges(); + } + }); + } + + /** + * Submit the current changes + * Bitstreams that were dragged around send out a patch request with move operations to the rest API + * Bitstreams marked as deleted send out a delete request to the rest API + * Display notifications and reset the current item/updates + */ + submit() { + this.submitting = true; + const bundlesOnce$ = this.bundles$.pipe(take(1)); + + // Fetch all move operations for each bundle + const moveOperations$ = bundlesOnce$.pipe( + switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) => + this.objectUpdatesService.getMoveOperations(bundle.self).pipe( + take(1), + map((operations: MoveOperation[]) => [...operations.map((operation: MoveOperation) => Object.assign(operation, { + from: `/_links/bitstreams${operation.from}/href`, + path: `/_links/bitstreams${operation.path}/href` + }))]) + ) + ))) + ); + + // Send out an immediate patch request for each bundle + const patchResponses$ = observableCombineLatest(bundlesOnce$, moveOperations$).pipe( + switchMap(([bundles, moveOperationList]: [Bundle[], Operation[][]]) => + observableZip(...bundles.map((bundle: Bundle, index: number) => { + if (isNotEmpty(moveOperationList[index])) { + return this.bundleService.patch(bundle, moveOperationList[index]); + } else { + return observableOf(undefined); + } + })) + ) + ); + + // Fetch all removed bitstreams from the object update service + const removedBitstreams$ = bundlesOnce$.pipe( + switchMap((bundles: Bundle[]) => observableZip( + ...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true)) + )), + map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat( + ...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)) + )), + map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field)) + ); + + // Send out delete requests for all deleted bitstreams + const removedResponses$ = removedBitstreams$.pipe( + take(1), + switchMap((removedBistreams: Bitstream[]) => { + if (isNotEmpty(removedBistreams)) { + return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.deleteAndReturnResponse(bitstream.id))); + } else { + return observableOf(undefined); + } + }) + ); + + // Perform the setup actions from above in order and display notifications + patchResponses$.pipe( + switchMap((responses: RestResponse[]) => { + this.displayNotifications('item.edit.bitstreams.notifications.move', responses); + return removedResponses$ + }), + take(1) + ).subscribe((responses: RestResponse[]) => { + this.displayNotifications('item.edit.bitstreams.notifications.remove', responses); + this.reset(); + this.submitting = false; + }); + } + + /** + * Display notifications + * - Error notification for each failed response with their message + * - Success notification in case there's at least one successful response + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displayNotifications(key: string, responses: RestResponse[]) { + if (isNotEmpty(responses)) { + const failedResponses = responses.filter((response: RestResponse) => hasValue(response) && !response.isSuccessful); + const successfulResponses = responses.filter((response: RestResponse) => hasValue(response) && response.isSuccessful); + + failedResponses.forEach((response: ErrorResponse) => { + this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage); + }); + if (successfulResponses.length > 0) { + this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); + } + } + } + + /** + * Request the object updates service to discard all current changes to this item + * Shows a notification to remind the user that they can undo this + */ + discard() { + const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), {timeOut: this.discardTimeOut}); + this.objectUpdatesService.discardAllFieldUpdates(this.url, undoNotification); + } + + /** + * Request the object updates service to undo discarding all changes to this item + */ + reinstate() { + this.bundles$.pipe(take(1)).subscribe((bundles: Bundle[]) => { + bundles.forEach((bundle: Bundle) => { + this.objectUpdatesService.reinstateFieldUpdates(bundle.self); + }); + }); + } + + /** + * Checks whether or not the object is currently reinstatable + */ + isReinstatable(): Observable { + return this.bundles$.pipe( + switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) => this.objectUpdatesService.isReinstatable(bundle.self)))), + map((reinstatable: boolean[]) => reinstatable.includes(true)) + ); + } + + /** + * Checks whether or not there are currently updates for this object + */ + hasChanges(): Observable { + return this.bundles$.pipe( + switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) => this.objectUpdatesService.hasUpdates(bundle.self)))), + map((hasChanges: boolean[]) => hasChanges.includes(true)) + ); + } + + /** + * De-cache the current item (it should automatically reload due to itemUpdateSubscription) + */ + reset() { + this.refreshItemCache(); + this.initializeItemUpdate(); + } + + /** + * Remove the current item's cache from object- and request-cache + */ + refreshItemCache() { + this.bundles$.pipe(take(1)).subscribe((bundles: Bundle[]) => { + bundles.forEach((bundle: Bundle) => { + this.objectCache.remove(bundle.self); + this.requestService.removeByHrefSubstring(bundle.self); + }); + this.objectCache.remove(this.item.self); + this.requestService.removeByHrefSubstring(this.item.self); + }); + } + + /** + * Unsubscribe from open subscriptions whenever the component gets destroyed + */ + ngOnDestroy(): void { + if (this.itemUpdateSubscription) { + this.itemUpdateSubscription.unsubscribe(); + } + } } diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html new file mode 100644 index 0000000000..58273bb931 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -0,0 +1,21 @@ + +
+
+ +
+ {{'item.edit.bitstreams.bundle.name' | translate:{ name: bundle.name } }} +
+
+
+
+ +
+
+
+ +
diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts new file mode 100644 index 0000000000..e15a9d7996 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -0,0 +1,58 @@ +import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA, ViewContainerRef } from '@angular/core'; +import { Item } from '../../../../core/shared/item.model'; +import { Bundle } from '../../../../core/shared/bundle.model'; +import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; + +describe('ItemEditBitstreamBundleComponent', () => { + let comp: ItemEditBitstreamBundleComponent; + let fixture: ComponentFixture; + let viewContainerRef: ViewContainerRef; + + const columnSizes = new ResponsiveTableSizes([ + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + new ResponsiveColumnSizes(6, 5, 4, 3, 3) + ]); + + const item = Object.assign(new Item(), { + id: 'item-1', + uuid: 'item-1' + }); + const bundle = Object.assign(new Bundle(), { + id: 'bundle-1', + uuid: 'bundle-1', + _links: { + self: { href: 'bundle-1-selflink' } + } + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ItemEditBitstreamBundleComponent], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemEditBitstreamBundleComponent); + comp = fixture.componentInstance; + comp.item = item; + comp.bundle = bundle; + comp.columnSizes = columnSizes; + viewContainerRef = (comp as any).viewContainerRef; + spyOn(viewContainerRef, 'createEmbeddedView'); + fixture.detectChanges(); + }); + + it('should create an embedded view of the component', () => { + expect(viewContainerRef.createEmbeddedView).toHaveBeenCalled(); + }); +}); diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts new file mode 100644 index 0000000000..115e326241 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { Bundle } from '../../../../core/shared/bundle.model'; +import { Item } from '../../../../core/shared/item.model'; +import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; + +@Component({ + selector: 'ds-item-edit-bitstream-bundle', + styleUrls: ['../item-bitstreams.component.scss'], + templateUrl: './item-edit-bitstream-bundle.component.html', +}) +/** + * Component that displays a single bundle of an item on the item bitstreams edit page + * Creates an embedded view of the contents. This is to ensure the table structure won't break. + * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element) + */ +export class ItemEditBitstreamBundleComponent implements OnInit { + + /** + * The view on the bundle information and bitstreams + */ + @ViewChild('bundleView', {static: true}) bundleView; + + /** + * The bundle to display bitstreams for + */ + @Input() bundle: Bundle; + + /** + * The item the bundle belongs to + */ + @Input() item: Item; + + /** + * The bootstrap sizes used for the columns within this table + */ + @Input() columnSizes: ResponsiveTableSizes; + + /** + * The bootstrap sizes used for the Bundle Name column + * This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit + */ + bundleNameColumn: ResponsiveColumnSizes; + + constructor(private viewContainerRef: ViewContainerRef) { + } + + ngOnInit(): void { + this.bundleNameColumn = this.columnSizes.combineColumns(0, 2); + this.viewContainerRef.createEmbeddedView(this.bundleView); + } +} diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html new file mode 100644 index 0000000000..25941f472e --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html @@ -0,0 +1,30 @@ + +
+
+ +
+ +
+
+
+
+
diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts new file mode 100644 index 0000000000..704fa0122e --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts @@ -0,0 +1,132 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Bundle } from '../../../../../core/shared/bundle.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { PaginatedDragAndDropBitstreamListComponent } from './paginated-drag-and-drop-bitstream-list.component'; +import { VarDirective } from '../../../../../shared/utils/var.directive'; +import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; +import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service'; +import { BundleDataService } from '../../../../../core/data/bundle-data.service'; +import { createMockRDObs } from '../../item-bitstreams.component.spec'; +import { Bitstream } from '../../../../../core/shared/bitstream.model'; +import { BitstreamFormat } from '../../../../../core/shared/bitstream-format.model'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { take } from 'rxjs/operators'; +import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes'; + +describe('PaginatedDragAndDropBitstreamListComponent', () => { + let comp: PaginatedDragAndDropBitstreamListComponent; + let fixture: ComponentFixture; + let objectUpdatesService: ObjectUpdatesService; + let bundleService: BundleDataService; + + const columnSizes = new ResponsiveTableSizes([ + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + new ResponsiveColumnSizes(6, 5, 4, 3, 3) + ]); + + const bundle = Object.assign(new Bundle(), { + id: 'bundle-1', + uuid: 'bundle-1', + _links: { + self: { href: 'bundle-1-selflink' } + } + }); + const date = new Date(); + const format = Object.assign(new BitstreamFormat(), { + shortDescription: 'PDF' + }); + const bitstream1 = Object.assign(new Bitstream(), { + uuid: 'bitstreamUUID1', + name: 'Fake Bitstream 1', + bundleName: 'ORIGINAL', + description: 'Description', + format: createMockRDObs(format) + }); + const fieldUpdate1 = { + field: bitstream1, + changeType: undefined + }; + const bitstream2 = Object.assign(new Bitstream(), { + uuid: 'bitstreamUUID2', + name: 'Fake Bitstream 2', + bundleName: 'ORIGINAL', + description: 'Description', + format: createMockRDObs(format) + }); + const fieldUpdate2 = { + field: bitstream2, + changeType: undefined + }; + + beforeEach(async(() => { + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', + { + getFieldUpdates: observableOf({ + [bitstream1.uuid]: fieldUpdate1, + [bitstream2.uuid]: fieldUpdate2, + }), + getFieldUpdatesExclusive: observableOf({ + [bitstream1.uuid]: fieldUpdate1, + [bitstream2.uuid]: fieldUpdate2, + }), + getFieldUpdatesByCustomOrder: observableOf({ + [bitstream1.uuid]: fieldUpdate1, + [bitstream2.uuid]: fieldUpdate2, + }), + saveMoveFieldUpdate: {}, + saveRemoveFieldUpdate: {}, + removeSingleFieldUpdate: {}, + saveAddFieldUpdate: {}, + discardFieldUpdates: {}, + reinstateFieldUpdates: observableOf(true), + initialize: {}, + getUpdatedFields: observableOf([bitstream1, bitstream2]), + getLastModified: observableOf(date), + hasUpdates: observableOf(true), + isReinstatable: observableOf(false), + isValidPage: observableOf(true), + initializeWithCustomOrder: {}, + addPageToCustomOrder: {} + } + ); + + bundleService = jasmine.createSpyObj('bundleService', { + getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])) + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective, ObjectValuesPipe], + providers: [ + { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: BundleDataService, useValue: bundleService } + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PaginatedDragAndDropBitstreamListComponent); + comp = fixture.componentInstance; + comp.bundle = bundle; + comp.columnSizes = columnSizes; + fixture.detectChanges(); + }); + + it('should initialize the objectsRD$', (done) => { + comp.objectsRD$.pipe(take(1)).subscribe((objects) => { + expect(objects.payload.page).toEqual([bitstream1, bitstream2]); + done(); + }); + }); + + it('should initialize the URL', () => { + expect(comp.url).toEqual(bundle.self); + }); +}); diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts new file mode 100644 index 0000000000..5548da4029 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts @@ -0,0 +1,63 @@ +import { AbstractPaginatedDragAndDropListComponent } from '../../../../../shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component'; +import { Component, ElementRef, Input, OnInit } from '@angular/core'; +import { Bundle } from '../../../../../core/shared/bundle.model'; +import { Bitstream } from '../../../../../core/shared/bitstream.model'; +import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service'; +import { BundleDataService } from '../../../../../core/data/bundle-data.service'; +import { switchMap } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../../../../shared/search/paginated-search-options.model'; +import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; + +@Component({ + selector: 'ds-paginated-drag-and-drop-bitstream-list', + styleUrls: ['../../item-bitstreams.component.scss'], + templateUrl: './paginated-drag-and-drop-bitstream-list.component.html', +}) +/** + * A component listing edit-bitstream rows for each bitstream within the given bundle. + * This component makes use of the AbstractPaginatedDragAndDropListComponent, allowing for users to drag and drop + * bitstreams within the paginated list. To drag and drop a bitstream between two pages, drag the row on top of the + * page number you want the bitstream to end up at. Doing so will add the bitstream to the top of that page. + */ +export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginatedDragAndDropListComponent implements OnInit { + /** + * The bundle to display bitstreams for + */ + @Input() bundle: Bundle; + + /** + * The bootstrap sizes used for the columns within this table + */ + @Input() columnSizes: ResponsiveTableSizes; + + constructor(protected objectUpdatesService: ObjectUpdatesService, + protected elRef: ElementRef, + protected bundleService: BundleDataService) { + super(objectUpdatesService, elRef); + } + + ngOnInit() { + super.ngOnInit(); + } + + /** + * Initialize the bitstreams observable depending on currentPage$ + */ + initializeObjectsRD(): void { + this.objectsRD$ = this.currentPage$.pipe( + switchMap((page: number) => this.bundleService.getBitstreams( + this.bundle.id, + new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}), + followLink('format') + )) + ); + } + + /** + * Initialize the URL used for the field-update store, in this case the bundle's self-link + */ + initializeURL(): void { + this.url = this.bundle.self; + } +} diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html new file mode 100644 index 0000000000..0561f78e97 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html @@ -0,0 +1,5 @@ + +
+ +
+
diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts new file mode 100644 index 0000000000..e6d72cbd57 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; + +@Component({ + selector: 'ds-item-edit-bitstream-drag-handle', + styleUrls: ['../item-bitstreams.component.scss'], + templateUrl: './item-edit-bitstream-drag-handle.component.html', +}) +/** + * Component displaying a drag handle for the item-edit-bitstream page + * Creates an embedded view of the contents + * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-drag-handle element) + */ +export class ItemEditBitstreamDragHandleComponent implements OnInit { + /** + * The view on the drag-handle + */ + @ViewChild('handleView', {static: true}) handleView; + + constructor(private viewContainerRef: ViewContainerRef) { + } + + ngOnInit(): void { + this.viewContainerRef.createEmbeddedView(this.handleView); + } + +} diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html new file mode 100644 index 0000000000..62014f06bd --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html @@ -0,0 +1,43 @@ + +
+ +
+ {{ bitstreamName }} +
+
+
+
+ {{ bitstream?.firstMetadataValue('dc.description') }} +
+
+
+
+ {{ (format$ | async)?.shortDescription }} +
+
+
+
+
+ + + + + + +
+
+
+
diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts new file mode 100644 index 0000000000..30b5e0d376 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts @@ -0,0 +1,119 @@ +import { ItemEditBitstreamComponent } from './item-edit-bitstream.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { Bitstream } from '../../../../core/shared/bitstream.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { VarDirective } from '../../../../shared/utils/var.directive'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { createMockRDObs } from '../item-bitstreams.component.spec'; +import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; +import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; + +let comp: ItemEditBitstreamComponent; +let fixture: ComponentFixture; + +const columnSizes = new ResponsiveTableSizes([ + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + new ResponsiveColumnSizes(6, 5, 4, 3, 3) +]); + +const format = Object.assign(new BitstreamFormat(), { + shortDescription: 'PDF' +}); +const bitstream = Object.assign(new Bitstream(), { + uuid: 'bitstreamUUID', + name: 'Fake Bitstream', + bundleName: 'ORIGINAL', + description: 'Description', + format: createMockRDObs(format) +}); +const fieldUpdate = { + field: bitstream, + changeType: undefined +}; +const date = new Date(); +const url = 'thisUrl'; + +let objectUpdatesService: ObjectUpdatesService; + +describe('ItemEditBitstreamComponent', () => { + beforeEach(async(() => { + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', + { + getFieldUpdates: observableOf({ + [bitstream.uuid]: fieldUpdate, + }), + getFieldUpdatesExclusive: observableOf({ + [bitstream.uuid]: fieldUpdate, + }), + saveRemoveFieldUpdate: {}, + removeSingleFieldUpdate: {}, + saveAddFieldUpdate: {}, + discardFieldUpdates: {}, + reinstateFieldUpdates: observableOf(true), + initialize: {}, + getUpdatedFields: observableOf([bitstream]), + getLastModified: observableOf(date), + hasUpdates: observableOf(true), + isReinstatable: observableOf(false), + isValidPage: observableOf(true) + } + ); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ItemEditBitstreamComponent, VarDirective], + providers: [ + { provide: ObjectUpdatesService, useValue: objectUpdatesService } + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemEditBitstreamComponent); + comp = fixture.componentInstance; + comp.fieldUpdate = fieldUpdate; + comp.bundleUrl = url; + comp.columnSizes = columnSizes; + comp.ngOnChanges(undefined); + fixture.detectChanges(); + }); + + describe('when remove is called', () => { + beforeEach(() => { + comp.remove(); + }); + + it('should call saveRemoveFieldUpdate on objectUpdatesService', () => { + expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, bitstream); + }); + }); + + describe('when undo is called', () => { + beforeEach(() => { + comp.undo(); + }); + + it('should call removeSingleFieldUpdate on objectUpdatesService', () => { + expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, bitstream.uuid); + }); + }); + + describe('when canRemove is called', () => { + it('should return true', () => { + expect(comp.canRemove()).toEqual(true) + }); + }); + + describe('when canUndo is called', () => { + it('should return false', () => { + expect(comp.canUndo()).toEqual(false) + }); + }); +}); diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts new file mode 100644 index 0000000000..5a02b9cac4 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts @@ -0,0 +1,110 @@ +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; +import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; +import { Bitstream } from '../../../../core/shared/bitstream.model'; +import { cloneDeep } from 'lodash'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; +import { Observable } from 'rxjs/internal/Observable'; +import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; + +@Component({ + selector: 'ds-item-edit-bitstream', + styleUrls: ['../item-bitstreams.component.scss'], + templateUrl: './item-edit-bitstream.component.html', +}) +/** + * Component that displays a single bitstream of an item on the edit page + * Creates an embedded view of the contents + * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream element) + */ +export class ItemEditBitstreamComponent implements OnChanges, OnInit { + + /** + * The view on the bitstream + */ + @ViewChild('bitstreamView', {static: true}) bitstreamView; + + /** + * The current field, value and state of the bitstream + */ + @Input() fieldUpdate: FieldUpdate; + + /** + * The url of the bundle + */ + @Input() bundleUrl: string; + + /** + * The bootstrap sizes used for the columns within this table + */ + @Input() columnSizes: ResponsiveTableSizes; + + /** + * The bitstream of this field + */ + bitstream: Bitstream; + + /** + * The bitstream's name + */ + bitstreamName: string; + + /** + * The format of the bitstream + */ + format$: Observable; + + constructor(private objectUpdatesService: ObjectUpdatesService, + private dsoNameService: DSONameService, + private viewContainerRef: ViewContainerRef) { + } + + ngOnInit(): void { + this.viewContainerRef.createEmbeddedView(this.bitstreamView); + } + + /** + * Update the current bitstream and its format on changes + * @param changes + */ + ngOnChanges(changes: SimpleChanges): void { + this.bitstream = cloneDeep(this.fieldUpdate.field) as Bitstream; + this.bitstreamName = this.dsoNameService.getName(this.bitstream); + this.format$ = this.bitstream.format.pipe( + getSucceededRemoteData(), + getRemoteDataPayload() + ); + } + + /** + * Sends a new remove update for this field to the object updates service + */ + remove(): void { + this.objectUpdatesService.saveRemoveFieldUpdate(this.bundleUrl, this.bitstream); + } + + /** + * Cancels the current update for this field in the object updates service + */ + undo(): void { + this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, this.bitstream.uuid); + } + + /** + * Check if a user should be allowed to remove this field + */ + canRemove(): boolean { + return this.fieldUpdate.changeType !== FieldChangeType.REMOVE; + } + + /** + * Check if a user should be allowed to cancel the update to this field + */ + canUndo(): boolean { + return this.fieldUpdate.changeType >= 0; + } + +} diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index 08e639ac68..6112f44a27 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -59,8 +59,8 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { * Initialize the values and updates of the current item's metadata fields */ public initializeUpdates(): void { - this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships()); - } + this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); + } /** * Initialize the prefix for notification messages @@ -81,7 +81,7 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { * Sends all initial values of this item to the object updates service */ public initializeOriginalFields() { - this.objectUpdatesService.initialize(this.url, this.getMetadataAsListExcludingRelationships(), this.item.lastModified); + this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified); } /** diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index 5caf0e3036..52faf96236 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -10,6 +10,7 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver'; import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; import { LinkService } from '../core/cache/builders/link.service'; +import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; export function getItemPageRoute(itemId: string) { return new URLCombiner(getItemModulePath(), itemId).toString(); @@ -20,6 +21,7 @@ export function getItemEditPath(id: string) { } const ITEM_EDIT_PATH = 'edit'; +const UPLOAD_BITSTREAM_PATH = 'bitstreams/new'; @NgModule({ imports: [ @@ -45,6 +47,11 @@ const ITEM_EDIT_PATH = 'edit'; path: ITEM_EDIT_PATH, loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule', canActivate: [AuthenticatedGuard] + }, + { + path: UPLOAD_BITSTREAM_PATH, + component: UploadBitstreamComponent, + canActivate: [AuthenticatedGuard] } ], } diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index 8d5d78ddd1..4c3a64e117 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -26,6 +26,7 @@ import { MetadataRepresentationListComponent } from './simple/metadata-represent import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component'; import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component'; import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component'; +import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; import { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component'; import { StatisticsModule } from '../statistics/statistics.module'; import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.component'; @@ -58,6 +59,7 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental- GenericItemPageFieldComponent, MetadataRepresentationListComponent, RelatedEntitiesSearchComponent, + UploadBitstreamComponent, TabbedRelatedEntitiesSearchComponent, AbstractIncrementalListComponent, ], diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2927cd4e65..258848ce83 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -28,6 +28,10 @@ const COMMUNITY_MODULE_PATH = 'communities'; export function getCommunityModulePath() { return `/${COMMUNITY_MODULE_PATH}`; } +const BITSTREAM_MODULE_PATH = 'bitstreams'; +export function getBitstreamModulePath() { + return `/${BITSTREAM_MODULE_PATH}`; +} const ADMIN_MODULE_PATH = 'admin'; @@ -63,6 +67,7 @@ export function getDSOPath(dso: DSpaceObject): string { { path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' }, { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, + { path: BITSTREAM_MODULE_PATH, loadChildren: './+bitstream-page/bitstream-page.module#BitstreamPageModule' }, { path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index 16d1b50b13..813b8d0f4f 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -4,6 +4,10 @@ import { ePeopleRegistryReducer, EPeopleRegistryState } from './+admin/admin-access-control/epeople-registry/epeople-registry.reducers'; +import { + groupRegistryReducer, + GroupRegistryState +} from './+admin/admin-access-control/group-registry/group-registry.reducers'; import { metadataRegistryReducer, MetadataRegistryState @@ -47,6 +51,7 @@ export interface AppState { relationshipLists: NameVariantListsState; communityList: CommunityListState; epeopleRegistry: EPeopleRegistryState; + groupRegistry: GroupRegistryState; } export const appReducers: ActionReducerMap = { @@ -66,6 +71,7 @@ export const appReducers: ActionReducerMap = { relationshipLists: nameVariantReducer, communityList: CommunityListReducer, epeopleRegistry: ePeopleRegistryReducer, + groupRegistry: groupRegistryReducer, }; export const routerStateSelector = (state: AppState) => state.router; diff --git a/src/app/core/cache/server-sync-buffer.reducer.ts b/src/app/core/cache/server-sync-buffer.reducer.ts index c86a0d5654..d79dd51da4 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.ts @@ -68,6 +68,8 @@ function addToServerSyncQueue(state: ServerSyncBufferState, action: AddToSSBActi const actionEntry = action.payload as ServerSyncBufferEntry; if (hasNoValue(state.buffer.find((entry) => entry.href === actionEntry.href && entry.method === actionEntry.method))) { return Object.assign({}, state, { buffer: state.buffer.concat(actionEntry) }); + } else { + return state; } } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 01a858113d..356dad5ed8 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -135,12 +135,16 @@ import { PoolTask } from './tasks/models/pool-task-object.model'; import { TaskObject } from './tasks/models/task-object.model'; import { PoolTaskDataService } from './tasks/pool-task-data.service'; import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; +import { ArrayMoveChangeAnalyzer } from './data/array-move-change-analyzer.service'; +import { BitstreamDataService } from './data/bitstream-data.service'; import { environment } from '../../environments/environment'; import { storeModuleConfig } from '../app.reducer'; import { VersionDataService } from './data/version-data.service'; import { VersionHistoryDataService } from './data/version-history-data.service'; import { Version } from './shared/version.model'; import { VersionHistory } from './shared/version-history.model'; +import { WorkflowActionDataService } from './data/workflow-action-data.service'; +import { WorkflowAction } from './tasks/models/workflow-action-object.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -231,6 +235,7 @@ const PROVIDERS = [ DSpaceObjectDataService, DSOChangeAnalyzer, DefaultChangeAnalyzer, + ArrayMoveChangeAnalyzer, ObjectSelectService, CSSVariableService, MenuService, @@ -242,6 +247,7 @@ const PROVIDERS = [ TaskResponseParsingService, ClaimedTaskDataService, PoolTaskDataService, + BitstreamDataService, EntityTypeService, ContentSourceResponseParsingService, SearchService, @@ -257,6 +263,7 @@ const PROVIDERS = [ VersionHistoryDataService, LicenseDataService, ItemTypeDataService, + WorkflowActionDataService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -306,7 +313,8 @@ export const models = ExternalSource, ExternalSourceEntry, Version, - VersionHistory + VersionHistory, + WorkflowAction ]; @NgModule({ diff --git a/src/app/core/data/array-move-change-analyzer.service.spec.ts b/src/app/core/data/array-move-change-analyzer.service.spec.ts new file mode 100644 index 0000000000..5f5388d935 --- /dev/null +++ b/src/app/core/data/array-move-change-analyzer.service.spec.ts @@ -0,0 +1,107 @@ +import { ArrayMoveChangeAnalyzer } from './array-move-change-analyzer.service'; +import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { Operation } from 'fast-json-patch'; + +/** + * Helper class for creating move tests + * Define a "from" and "to" index to move objects within the array before comparing + */ +class MoveTest { + from: number; + to: number; + + constructor(from: number, to: number) { + this.from = from; + this.to = to; + } +} + +describe('ArrayMoveChangeAnalyzer', () => { + const comparator = new ArrayMoveChangeAnalyzer(); + + let originalArray = []; + + describe('when all values are defined', () => { + beforeEach(() => { + originalArray = [ + '98700118-d65d-4636-b1d0-dba83fc932e1', + '4d7d0798-a8fa-45b8-b4fc-deb2819606c8', + 'e56eb99e-2f7c-4bee-9b3f-d3dcc83386b1', + '0f608168-cdfc-46b0-92ce-889f7d3ac684', + '546f9f5c-15dc-4eec-86fe-648007ac9e1c' + ]; + }); + + testMove([ + { op: 'move', from: '/2', path: '/4' }, + ], new MoveTest(2, 4)); + + testMove([ + { op: 'move', from: '/0', path: '/3' }, + ], new MoveTest(0, 3)); + + testMove([ + { op: 'move', from: '/0', path: '/3' }, + { op: 'move', from: '/2', path: '/1' } + ], new MoveTest(0, 3), new MoveTest(1, 2)); + + testMove([ + { op: 'move', from: '/0', path: '/1' }, + { op: 'move', from: '/3', path: '/4' } + ], new MoveTest(0, 1), new MoveTest(3, 4)); + + testMove([], new MoveTest(0, 4), new MoveTest(4, 0)); + + testMove([ + { op: 'move', from: '/0', path: '/3' }, + { op: 'move', from: '/2', path: '/1' } + ], new MoveTest(0, 4), new MoveTest(1, 3), new MoveTest(2, 4)); + }); + + describe('when some values are undefined (index 2 and 3)', () => { + beforeEach(() => { + originalArray = [ + '98700118-d65d-4636-b1d0-dba83fc932e1', + '4d7d0798-a8fa-45b8-b4fc-deb2819606c8', + undefined, + undefined, + '546f9f5c-15dc-4eec-86fe-648007ac9e1c' + ]; + }); + + // It can't create a move operation for undefined values, so it should create move operations for the defined values instead + testMove([ + { op: 'move', from: '/4', path: '/3' }, + ], new MoveTest(2, 4)); + + // Moving a defined value should result in the same operations + testMove([ + { op: 'move', from: '/0', path: '/3' }, + ], new MoveTest(0, 3)); + }); + + /** + * Helper function for creating a move test + * + * @param expectedOperations An array of expected operations after comparing the original array with the array + * created using the provided MoveTests + * @param moves An array of MoveTest objects telling the test where to move objects before comparing + */ + function testMove(expectedOperations: Operation[], ...moves: MoveTest[]) { + describe(`move ${moves.map((move) => `${move.from} to ${move.to}`).join(' and ')}`, () => { + let result; + + beforeEach(() => { + const movedArray = [...originalArray]; + moves.forEach((move) => { + moveItemInArray(movedArray, move.from, move.to); + }); + result = comparator.diff(originalArray, movedArray); + }); + + it('should create the expected move operations', () => { + expect(result).toEqual(expectedOperations); + }); + }); + } +}); diff --git a/src/app/core/data/array-move-change-analyzer.service.ts b/src/app/core/data/array-move-change-analyzer.service.ts new file mode 100644 index 0000000000..39d22fc463 --- /dev/null +++ b/src/app/core/data/array-move-change-analyzer.service.ts @@ -0,0 +1,37 @@ +import { MoveOperation } from 'fast-json-patch/lib/core'; +import { Injectable } from '@angular/core'; +import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { hasValue } from '../../shared/empty.util'; + +/** + * A class to determine move operations between two arrays + */ +@Injectable() +export class ArrayMoveChangeAnalyzer { + + /** + * Compare two arrays detecting and returning move operations + * + * @param array1 The original array + * @param array2 The custom array to compare with the original + */ + diff(array1: T[], array2: T[]): MoveOperation[] { + const result = []; + const moved = [...array1]; + array1.forEach((value: T, index: number) => { + if (hasValue(value)) { + const otherIndex = array2.indexOf(value); + const movedIndex = moved.indexOf(value); + if (index !== otherIndex && movedIndex !== otherIndex) { + moveItemInArray(moved, movedIndex, otherIndex); + result.push(Object.assign({ + op: 'move', + from: '/' + movedIndex, + path: '/' + otherIndex + }) as MoveOperation) + } + } + }); + return result; + } +} diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts new file mode 100644 index 0000000000..fca0f6b650 --- /dev/null +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -0,0 +1,58 @@ +import { BitstreamDataService } from './bitstream-data.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RequestService } from './request.service'; +import { Bitstream } from '../shared/bitstream.model'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level'; +import { PutRequest } from './request.models'; + +describe('BitstreamDataService', () => { + let service: BitstreamDataService; + let objectCache: ObjectCacheService; + let requestService: RequestService; + let halService: HALEndpointService; + let bitstreamFormatService: BitstreamFormatDataService; + const bitstreamFormatHref = 'rest-api/bitstreamformats'; + + const bitstream = Object.assign(new Bitstream(), { + uuid: 'fake-bitstream', + _links: { + self: { href: 'fake-bitstream-self' } + } + }); + const format = Object.assign(new BitstreamFormat(), { + id: '2', + shortDescription: 'PNG', + description: 'Portable Network Graphics', + supportLevel: BitstreamFormatSupportLevel.Known + }); + const url = 'fake-bitstream-url'; + + beforeEach(() => { + objectCache = jasmine.createSpyObj('objectCache', { + remove: jasmine.createSpy('remove') + }); + requestService = getMockRequestService(); + halService = Object.assign(new HALEndpointServiceStub(url)); + bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', { + getBrowseEndpoint: observableOf(bitstreamFormatHref) + }); + + service = new BitstreamDataService(requestService, null, null, null, objectCache, halService, null, null, null, null, bitstreamFormatService); + }); + + describe('when updating the bitstream\'s format', () => { + beforeEach(() => { + service.updateFormat(bitstream, format); + }); + + it('should configure a put request', () => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest)); + }); + }); +}); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index c571c7f96c..4c24f5d78b 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -1,8 +1,8 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/internal/Observable'; -import { map, switchMap } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -22,8 +22,14 @@ import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; import { RemoteDataError } from './remote-data-error'; -import { FindListOptions } from './request.models'; +import { FindListOptions, PutRequest } from './request.models'; import { RequestService } from './request.service'; +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { RestResponse } from '../cache/response.models'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { combineLatest as observableCombineLatest } from 'rxjs'; /** * A service to retrieve {@link Bitstream}s from the REST API @@ -50,6 +56,7 @@ export class BitstreamDataService extends DataService { protected http: HttpClient, protected comparator: DSOChangeAnalyzer, protected bundleService: BundleDataService, + protected bitstreamFormatService: BitstreamFormatDataService ) { super(); } @@ -167,4 +174,37 @@ export class BitstreamDataService extends DataService { ); } + /** + * Set the format of a bitstream + * @param bitstream + * @param format + */ + updateFormat(bitstream: Bitstream, format: BitstreamFormat): Observable { + const requestId = this.requestService.generateRequestId(); + const bitstreamHref$ = this.getBrowseEndpoint().pipe( + map((href: string) => `${href}/${bitstream.id}`), + switchMap((href: string) => this.halService.getEndpoint('format', href)) + ); + const formatHref$ = this.bitstreamFormatService.getBrowseEndpoint().pipe( + map((href: string) => `${href}/${format.id}`) + ); + observableCombineLatest([bitstreamHref$, formatHref$]).pipe( + map(([bitstreamHref, formatHref]) => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + return new PutRequest(requestId, bitstreamHref, formatHref, options); + }), + configureRequest(this.requestService), + take(1) + ).subscribe(() => { + this.requestService.removeByHrefSubstring(bitstream.self + '/format'); + }); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry() + ); + } + } diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index 64d58eb8ec..160ea0ff0d 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/internal/Observable'; -import { map } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -18,8 +18,10 @@ import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; -import { FindListOptions } from './request.models'; +import { FindListOptions, GetRequest } from './request.models'; import { RequestService } from './request.service'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { Bitstream } from '../shared/bitstream.model'; /** * A service to retrieve {@link Bundle}s from the REST API @@ -30,6 +32,7 @@ import { RequestService } from './request.service'; @dataService(BUNDLE) export class BundleDataService extends DataService { protected linkPath = 'bundles'; + protected bitstreamsEndpoint = 'bitstreams'; constructor( protected requestService: RequestService, @@ -81,4 +84,34 @@ export class BundleDataService extends DataService { }), ); } + + /** + * Get the bitstreams endpoint for a bundle + * @param bundleId + */ + getBitstreamsEndpoint(bundleId: string): Observable { + return this.getBrowseEndpoint().pipe( + switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`)) + ); + } + + /** + * Get a bundle's bitstreams using paginated search options + * @param bundleId The bundle's ID + * @param searchOptions The search options to use + * @param linksToFollow The {@link FollowLinkConfig}s for the request + */ + getBitstreams(bundleId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array>): Observable>> { + const hrefObs = this.getBitstreamsEndpoint(bundleId).pipe( + map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) + ); + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(hrefObs, ...linksToFollow); + } } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 135834b430..7cbfb2ad03 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -14,7 +14,7 @@ import { take, tap } from 'rxjs/operators'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -44,7 +44,8 @@ import { FindByIDRequest, FindListOptions, FindListRequest, - GetRequest, PatchRequest + GetRequest, + PatchRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; @@ -475,6 +476,39 @@ export abstract class DataService { * @return an observable that emits true when the deletion was successful, false when it failed */ delete(dsoID: string, copyVirtualMetadata?: string[]): Observable { + const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response.isSuccessful) + ); + } + + /** + * Delete an existing DSpace Object on the server + * @param dsoID The DSpace Object' id to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * metadata should be saved as real metadata + * Return an observable of the completed response + */ + deleteAndReturnResponse(dsoID: string, copyVirtualMetadata?: string[]): Observable { + const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata); + + return this.requestService.getByUUID(requestId).pipe( + hasValueOperator(), + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response) + ); + } + + /** + * Delete an existing DSpace Object on the server + * @param dsoID The DSpace Object' id to be removed + * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual + * metadata should be saved as real metadata + * Return the delete request's ID + */ + private deleteAndReturnRequestId(dsoID: string, copyVirtualMetadata?: string[]): string { const requestId = this.requestService.generateRequestId(); const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( @@ -495,10 +529,7 @@ export abstract class DataService { }) ).subscribe(); - return this.requestService.getByUUID(requestId).pipe( - find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response.isSuccessful) - ); + return requestId; } /** diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 53ca4ffe41..282a43ec61 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -47,6 +47,9 @@ describe('ItemDataService', () => { return cold('a', { a: itemEndpoint }); } } as HALEndpointService; + const bundleService = jasmine.createSpyObj('bundleService', { + findByHref: {} + }); const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39'; const options = Object.assign(new FindListOptions(), { @@ -87,7 +90,8 @@ describe('ItemDataService', () => { halEndpointService, notificationsService, http, - comparator + comparator, + bundleService ); } @@ -212,4 +216,20 @@ describe('ItemDataService', () => { }); }); + describe('createBundle', () => { + const itemId = '3de6ea60-ec39-419b-ae6f-065930ac1429'; + const bundleName = 'ORIGINAL'; + let result; + + beforeEach(() => { + service = initTestService(); + spyOn(requestService, 'configure'); + result = service.createBundle(itemId, bundleName); + }); + + it('should configure a POST request', () => { + result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest))); + }); + }); + }); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index a23eb27f4a..562050c802 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, find, map, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, find, map, switchMap, take } from 'rxjs/operators'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { BrowseService } from '../browse/browse.service'; @@ -32,6 +32,7 @@ import { RemoteData } from './remote-data'; import { DeleteRequest, FindListOptions, + GetRequest, MappedCollectionsRequest, PatchRequest, PostRequest, @@ -40,6 +41,10 @@ import { } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { Bundle } from '../shared/bundle.model'; +import { MetadataMap } from '../shared/metadata.models'; +import { BundleDataService } from './bundle-data.service'; @Injectable() @dataService(ITEM) @@ -56,6 +61,7 @@ export class ItemDataService extends DataService { protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DSOChangeAnalyzer, + protected bundleService: BundleDataService ) { super(); } @@ -219,6 +225,76 @@ export class ItemDataService extends DataService { ); } + /** + * Get the endpoint for an item's bundles + * @param itemId + */ + public getBundlesEndpoint(itemId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((url: string) => this.halService.getEndpoint('bundles', `${url}/${itemId}`)) + ); + } + + /** + * Get an item's bundles using paginated search options + * @param itemId The item's ID + * @param searchOptions The search options to use + */ + public getBundles(itemId: string, searchOptions?: PaginatedSearchOptions): Observable>> { + const hrefObs = this.getBundlesEndpoint(itemId).pipe( + map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) + ); + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(hrefObs); + } + + /** + * Create a new bundle on an item + * @param itemId The item's ID + * @param bundleName The new bundle's name + * @param metadata Optional metadata for the bundle + */ + public createBundle(itemId: string, bundleName: string, metadata?: MetadataMap): Observable> { + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getBundlesEndpoint(itemId); + + const bundleJson = { + name: bundleName, + metadata: metadata ? metadata : {} + }; + + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/json'); + options.headers = headers; + const request = new PostRequest(requestId, href, JSON.stringify(bundleJson), options); + this.requestService.configure(request); + }); + + const selfLink$ = this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + map((response: any) => { + if (isNotEmpty(response.resourceSelfLinks)) { + return response.resourceSelfLinks[0]; + } + }), + distinctUntilChanged() + ) as Observable; + + return selfLink$.pipe( + switchMap((selfLink: string) => this.bundleService.findByHref(selfLink)), + ); + } + /** * Get the endpoint to move the item * @param itemId diff --git a/src/app/core/data/object-updates/object-updates.actions.ts b/src/app/core/data/object-updates/object-updates.actions.ts index 9df9acec8f..94918157ee 100644 --- a/src/app/core/data/object-updates/object-updates.actions.ts +++ b/src/app/core/data/object-updates/object-updates.actions.ts @@ -8,6 +8,7 @@ import {INotification} from '../../../shared/notifications/models/notification.m */ export const ObjectUpdatesActionTypes = { INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'), + ADD_PAGE_TO_CUSTOM_ORDER: type('dspace/core/cache/object-updates/ADD_PAGE_TO_CUSTOM_ORDER'), SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'), SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'), ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'), @@ -15,7 +16,9 @@ export const ObjectUpdatesActionTypes = { DISCARD: type('dspace/core/cache/object-updates/DISCARD'), REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'), REMOVE: type('dspace/core/cache/object-updates/REMOVE'), + REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'), REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'), + MOVE: type('dspace/core/cache/object-updates/MOVE'), }; /* tslint:disable:max-classes-per-file */ @@ -26,7 +29,8 @@ export const ObjectUpdatesActionTypes = { export enum FieldChangeType { UPDATE = 0, ADD = 1, - REMOVE = 2 + REMOVE = 2, + MOVE = 3 } /** @@ -37,7 +41,10 @@ export class InitializeFieldsAction implements Action { payload: { url: string, fields: Identifiable[], - lastModified: Date + lastModified: Date, + order: string[], + pageSize: number, + page: number }; /** @@ -47,13 +54,49 @@ export class InitializeFieldsAction implements Action { * the unique url of the page for which the fields are being initialized * @param fields The identifiable fields of which the updates are kept track of * @param lastModified The last modified date of the object that belongs to the page + * @param order A custom order to keep track of objects moving around + * @param pageSize The page size used to fill empty pages for the custom order + * @param page The first page to populate in the custom order */ constructor( url: string, fields: Identifiable[], - lastModified: Date + lastModified: Date, + order: string[] = [], + pageSize: number = 9999, + page: number = 0 ) { - this.payload = { url, fields, lastModified }; + this.payload = { url, fields, lastModified, order, pageSize, page }; + } +} + +/** + * An ngrx action to initialize a new page's fields in the ObjectUpdates state + */ +export class AddPageToCustomOrderAction implements Action { + type = ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER; + payload: { + url: string, + fields: Identifiable[], + order: string[], + page: number + }; + + /** + * Create a new AddPageToCustomOrderAction + * + * @param url The unique url of the page for which the fields are being added + * @param fields The identifiable fields of which the updates are kept track of + * @param order A custom order to keep track of objects moving around + * @param page The page to populate in the custom order + */ + constructor( + url: string, + fields: Identifiable[], + order: string[] = [], + page: number = 0 + ) { + this.payload = { url, fields, order, page }; } } @@ -180,7 +223,8 @@ export class DiscardObjectUpdatesAction implements Action { type = ObjectUpdatesActionTypes.DISCARD; payload: { url: string, - notification: INotification + notification: INotification, + discardAll: boolean; }; /** @@ -189,12 +233,14 @@ export class DiscardObjectUpdatesAction implements Action { * @param url * the unique url of the page for which the changes should be discarded * @param notification The notification that is raised when changes are discarded + * @param discardAll discard all */ constructor( url: string, - notification: INotification + notification: INotification, + discardAll = false ) { - this.payload = { url, notification }; + this.payload = { url, notification, discardAll }; } } @@ -242,6 +288,13 @@ export class RemoveObjectUpdatesAction implements Action { } } +/** + * An ngrx action to remove all previously discarded updates in the ObjectUpdates state + */ +export class RemoveAllObjectUpdatesAction implements Action { + type = ObjectUpdatesActionTypes.REMOVE_ALL; +} + /** * An ngrx action to remove a single field update in the ObjectUpdates state for a certain page url and field uuid */ @@ -267,6 +320,43 @@ export class RemoveFieldUpdateAction implements Action { } } +/** + * An ngrx action to remove a single field update in the ObjectUpdates state for a certain page url and field uuid + */ +export class MoveFieldUpdateAction implements Action { + type = ObjectUpdatesActionTypes.MOVE; + payload: { + url: string, + from: number, + to: number, + fromPage: number, + toPage: number, + field?: Identifiable + }; + + /** + * Create a new RemoveObjectUpdatesAction + * + * @param url + * the unique url of the page for which a field's change should be removed + * @param from The index of the object to move + * @param to The index to move the object to + * @param fromPage The page to move the object from + * @param toPage The page to move the object to + * @param field Optional field to add to the fieldUpdates list (useful when we want to track updates across multiple pages) + */ + constructor( + url: string, + from: number, + to: number, + fromPage: number, + toPage: number, + field?: Identifiable + ) { + this.payload = { url, from, to, fromPage, toPage, field }; + } +} + /* tslint:enable:max-classes-per-file */ /** @@ -279,6 +369,9 @@ export type ObjectUpdatesAction | ReinstateObjectUpdatesAction | RemoveObjectUpdatesAction | RemoveFieldUpdateAction + | MoveFieldUpdateAction + | AddPageToCustomOrderAction + | RemoveAllObjectUpdatesAction | SelectVirtualMetadataAction | SetEditableFieldUpdateAction | SetValidFieldUpdateAction; diff --git a/src/app/core/data/object-updates/object-updates.effects.ts b/src/app/core/data/object-updates/object-updates.effects.ts index 88cd3bc718..239fee9477 100644 --- a/src/app/core/data/object-updates/object-updates.effects.ts +++ b/src/app/core/data/object-updates/object-updates.effects.ts @@ -3,12 +3,12 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { DiscardObjectUpdatesAction, ObjectUpdatesAction, - ObjectUpdatesActionTypes, + ObjectUpdatesActionTypes, RemoveAllObjectUpdatesAction, RemoveObjectUpdatesAction } from './object-updates.actions'; import { delay, filter, map, switchMap, take, tap } from 'rxjs/operators'; import { of as observableOf, race as observableRace, Subject } from 'rxjs'; -import { hasNoValue } from '../../../shared/empty.util'; +import { hasNoValue, hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { INotification } from '../../../shared/notifications/models/notification.model'; import { @@ -16,6 +16,7 @@ import { NotificationsActionTypes, RemoveNotificationAction } from '../../../shared/notifications/notifications.actions'; +import { Action } from '@ngrx/store'; /** * NGRX effects for ObjectUpdatesActions @@ -53,13 +54,14 @@ export class ObjectUpdatesEffects { .pipe( ofType(...Object.values(ObjectUpdatesActionTypes)), map((action: ObjectUpdatesAction) => { - const url: string = action.payload.url; + if (hasValue((action as any).payload)) { + const url: string = (action as any).payload.url; if (hasNoValue(this.actionMap$[url])) { this.actionMap$[url] = new Subject(); } this.actionMap$[url].next(action); } - ) + }) ); /** @@ -91,9 +93,15 @@ export class ObjectUpdatesEffects { const url: string = action.payload.url; const notification: INotification = action.payload.notification; const timeOut = notification.options.timeOut; + + let removeAction: Action = new RemoveObjectUpdatesAction(action.payload.url); + if (action.payload.discardAll) { + removeAction = new RemoveAllObjectUpdatesAction(); + } + return observableRace( // Either wait for the delay and perform a remove action - observableOf(new RemoveObjectUpdatesAction(action.payload.url)).pipe(delay(timeOut)), + observableOf(removeAction).pipe(delay(timeOut)), // Or wait for a a user action this.actionMap$[url].pipe( take(1), @@ -106,19 +114,19 @@ export class ObjectUpdatesEffects { return { type: 'NO_ACTION' } } // If someone performed another action, assume the user does not want to reinstate and remove all changes - return new RemoveObjectUpdatesAction(action.payload.url); + return removeAction }) ), this.notificationActionMap$[notification.id].pipe( filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_NOTIFICATION), map(() => { - return new RemoveObjectUpdatesAction(action.payload.url); + return removeAction; }) ), this.notificationActionMap$[this.allIdentifier].pipe( filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_ALL_NOTIFICATIONS), map(() => { - return new RemoveObjectUpdatesAction(action.payload.url); + return removeAction; }) ) ) diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts index faae4732bc..bdf202049e 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts @@ -1,10 +1,10 @@ import * as deepFreeze from 'deep-freeze'; import { - AddFieldUpdateAction, + AddFieldUpdateAction, AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, - InitializeFieldsAction, - ReinstateObjectUpdatesAction, + InitializeFieldsAction, MoveFieldUpdateAction, + ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction, RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction } from './object-updates.actions'; @@ -85,6 +85,16 @@ describe('objectUpdatesReducer', () => { virtualMetadataSources: { [relationship.uuid]: {[identifiable1.uuid]: true} }, + customOrder: { + initialOrderPages: [ + { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } + ], + newOrderPages: [ + { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } + ], + pageSize: 10, + changed: false + } } }; @@ -111,6 +121,16 @@ describe('objectUpdatesReducer', () => { virtualMetadataSources: { [relationship.uuid]: {[identifiable1.uuid]: true} }, + customOrder: { + initialOrderPages: [ + { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } + ], + newOrderPages: [ + { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } + ], + pageSize: 10, + changed: false + } }, [url + OBJECT_UPDATES_TRASH_PATH]: { fieldStates: { @@ -145,6 +165,16 @@ describe('objectUpdatesReducer', () => { virtualMetadataSources: { [relationship.uuid]: {[identifiable1.uuid]: true} }, + customOrder: { + initialOrderPages: [ + { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } + ], + newOrderPages: [ + { order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] } + ], + pageSize: 10, + changed: false + } } }; @@ -213,7 +243,7 @@ describe('objectUpdatesReducer', () => { }); it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => { - const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate); + const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate, [identifiable1.uuid, identifiable3.uuid], 10, 0); const expectedState = { [url]: { @@ -231,7 +261,17 @@ describe('objectUpdatesReducer', () => { }, fieldUpdates: {}, virtualMetadataSources: {}, - lastModified: modDate + lastModified: modDate, + customOrder: { + initialOrderPages: [ + { order: [identifiable1.uuid, identifiable3.uuid] } + ], + newOrderPages: [ + { order: [identifiable1.uuid, identifiable3.uuid] } + ], + pageSize: 10, + changed: false + } } }; const newState = objectUpdatesReducer(testState, action); @@ -283,10 +323,44 @@ describe('objectUpdatesReducer', () => { expect(newState[url + OBJECT_UPDATES_TRASH_PATH]).toBeUndefined(); }); + it('should remove all updates from the state when the REMOVE_ALL action is dispatched', () => { + const action = new RemoveAllObjectUpdatesAction(); + + const newState = objectUpdatesReducer(discardedTestState, action as any); + expect(newState[url].fieldUpdates).toBeUndefined(); + expect(newState[url + OBJECT_UPDATES_TRASH_PATH]).toBeUndefined(); + }); + it('should remove a given field\'s update from the state when the REMOVE_FIELD action is dispatched, based on the payload', () => { const action = new RemoveFieldUpdateAction(url, uuid); const newState = objectUpdatesReducer(testState, action); expect(newState[url].fieldUpdates[uuid]).toBeUndefined(); }); + + it('should move the custom order from the state when the MOVE action is dispatched', () => { + const action = new MoveFieldUpdateAction(url, 0, 1, 0, 0); + + const newState = objectUpdatesReducer(testState, action); + expect(newState[url].customOrder.newOrderPages[0].order[0]).toEqual(testState[url].customOrder.newOrderPages[0].order[1]); + expect(newState[url].customOrder.newOrderPages[0].order[1]).toEqual(testState[url].customOrder.newOrderPages[0].order[0]); + expect(newState[url].customOrder.changed).toEqual(true); + }); + + it('should add a new page to the custom order and add empty pages in between when the ADD_PAGE_TO_CUSTOM_ORDER action is dispatched', () => { + const identifiable4 = { + uuid: 'a23eae5a-7857-4ef9-8e52-989436ad2955', + key: 'dc.description.abstract', + language: null, + value: 'Extra value' + }; + const action = new AddPageToCustomOrderAction(url, [identifiable4], [identifiable4.uuid], 2); + + const newState = objectUpdatesReducer(testState, action); + // Confirm the page in between the two pages (index 1) has been filled with 10 (page size) undefined values + expect(newState[url].customOrder.newOrderPages[1].order.length).toEqual(10); + expect(newState[url].customOrder.newOrderPages[1].order[0]).toBeUndefined(); + // Verify the new page is correct + expect(newState[url].customOrder.newOrderPages[2].order[0]).toEqual(identifiable4.uuid); + }); }); diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index cffd41856d..759a9f5c87 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -1,8 +1,8 @@ import { - AddFieldUpdateAction, + AddFieldUpdateAction, AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, - InitializeFieldsAction, + InitializeFieldsAction, MoveFieldUpdateAction, ObjectUpdatesAction, ObjectUpdatesActionTypes, ReinstateObjectUpdatesAction, @@ -12,7 +12,9 @@ import { SetValidFieldUpdateAction, SelectVirtualMetadataAction, } from './object-updates.actions'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; +import { from } from 'rxjs/internal/observable/from'; import {Relationship} from '../../shared/item-relationships/relationship.model'; /** @@ -46,7 +48,7 @@ export interface Identifiable { /** * The state of a single field update */ -export interface FieldUpdate { +export interface FieldUpdate { field: Identifiable, changeType: FieldChangeType } @@ -81,6 +83,20 @@ export interface DeleteRelationship extends Relationship { keepRightVirtualMetadata: boolean, } +/** + * A custom order given to the list of objects + */ +export interface CustomOrder { + initialOrderPages: OrderPage[], + newOrderPages: OrderPage[], + pageSize: number; + changed: boolean +} + +export interface OrderPage { + order: string[] +} + /** * The updated state of a single page */ @@ -89,6 +105,7 @@ export interface ObjectUpdatesEntry { fieldUpdates: FieldUpdates; virtualMetadataSources: VirtualMetadataSources; lastModified: Date; + customOrder: CustomOrder } /** @@ -121,6 +138,9 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: { return initializeFieldsUpdate(state, action as InitializeFieldsAction); } + case ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER: { + return addPageToCustomOrder(state, action as AddPageToCustomOrderAction); + } case ObjectUpdatesActionTypes.ADD_FIELD: { return addFieldUpdate(state, action as AddFieldUpdateAction); } @@ -136,6 +156,9 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates case ObjectUpdatesActionTypes.REMOVE: { return removeObjectUpdates(state, action as RemoveObjectUpdatesAction); } + case ObjectUpdatesActionTypes.REMOVE_ALL: { + return removeAllObjectUpdates(state); + } case ObjectUpdatesActionTypes.REMOVE_FIELD: { return removeFieldUpdate(state, action as RemoveFieldUpdateAction); } @@ -145,6 +168,9 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates case ObjectUpdatesActionTypes.SET_VALID_FIELD: { return setValidFieldUpdate(state, action as SetValidFieldUpdateAction); } + case ObjectUpdatesActionTypes.MOVE: { + return moveFieldUpdate(state, action as MoveFieldUpdateAction); + } default: { return state; } @@ -160,18 +186,50 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) { const url: string = action.payload.url; const fields: Identifiable[] = action.payload.fields; const lastModifiedServer: Date = action.payload.lastModified; + const order = action.payload.order; + const pageSize = action.payload.pageSize; + const page = action.payload.page; const fieldStates = createInitialFieldStates(fields); + const initialOrderPages = addOrderToPages([], order, pageSize, page); const newPageState = Object.assign( {}, state[url], { fieldStates: fieldStates }, { fieldUpdates: {} }, { virtualMetadataSources: {} }, - { lastModified: lastModifiedServer } + { lastModified: lastModifiedServer }, + { customOrder: { + initialOrderPages: initialOrderPages, + newOrderPages: initialOrderPages, + pageSize: pageSize, + changed: false } + } ); return Object.assign({}, state, { [url]: newPageState }); } +/** + * Add a page of objects to the state of a specific url and update a specific page of the custom order + * @param state The current state + * @param action The action to perform on the current state + */ +function addPageToCustomOrder(state: any, action: AddPageToCustomOrderAction) { + const url: string = action.payload.url; + const fields: Identifiable[] = action.payload.fields; + const fieldStates = createInitialFieldStates(fields); + const order = action.payload.order; + const page = action.payload.page; + const pageState: ObjectUpdatesEntry = state[url] || {}; + const newPageState = Object.assign({}, pageState, { + fieldStates: Object.assign({}, pageState.fieldStates, fieldStates), + customOrder: Object.assign({}, pageState.customOrder, { + newOrderPages: addOrderToPages(pageState.customOrder.newOrderPages, order, pageState.customOrder.pageSize, page), + initialOrderPages: addOrderToPages(pageState.customOrder.initialOrderPages, order, pageState.customOrder.pageSize, page) + }) + }); + return Object.assign({}, state, { [url]: newPageState }); +} + /** * Add a new update for a specific field to the store * @param state The current state @@ -252,7 +310,24 @@ function selectVirtualMetadata(state: any, action: SelectVirtualMetadataAction) * @param action The action to perform on the current state */ function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) { - const url: string = action.payload.url; + if (action.payload.discardAll) { + let newState = Object.assign({}, state); + Object.keys(state).filter((path: string) => !path.endsWith(OBJECT_UPDATES_TRASH_PATH)).forEach((path: string) => { + newState = discardObjectUpdatesFor(path, newState); + }); + return newState; + } else { + const url: string = action.payload.url; + return discardObjectUpdatesFor(url, state); + } +} + +/** + * Discard all updates for a specific action's url in the store + * @param url The action's url + * @param state The current state + */ +function discardObjectUpdatesFor(url: string, state: any) { const pageState: ObjectUpdatesEntry = state[url]; const newFieldStates = {}; Object.keys(pageState.fieldStates).forEach((uuid: string) => { @@ -263,9 +338,19 @@ function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) { } }); + const newCustomOrder = Object.assign({}, pageState.customOrder); + if (pageState.customOrder.changed) { + const initialOrder = pageState.customOrder.initialOrderPages; + if (isNotEmpty(initialOrder)) { + newCustomOrder.newOrderPages = initialOrder; + newCustomOrder.changed = false; + } + } + const discardedPageState = Object.assign({}, pageState, { fieldUpdates: {}, - fieldStates: newFieldStates + fieldStates: newFieldStates, + customOrder: newCustomOrder }); return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState }); } @@ -305,6 +390,18 @@ function removeObjectUpdatesByURL(state: any, url: string) { return newState; } +/** + * Remove all updates in the store + * @param state The current state + */ +function removeAllObjectUpdates(state: any) { + const newState = Object.assign({}, state); + Object.keys(state).filter((path: string) => path.endsWith(OBJECT_UPDATES_TRASH_PATH)).forEach((path: string) => { + delete newState[path]; + }); + return newState; +} + /** * Discard the update for a specific action's url and field UUID in the store * @param state The current state @@ -407,3 +504,121 @@ function createInitialFieldStates(fields: Identifiable[]) { uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState); return fieldStates; } + +/** + * Method to add a list of objects to an existing FieldStates object + * @param fieldStates FieldStates to add states to + * @param fields Identifiable objects The list of objects to add to the FieldStates + */ +function addFieldStates(fieldStates: FieldStates, fields: Identifiable[]) { + const uuids = fields.map((field: Identifiable) => field.uuid); + uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState); + return fieldStates; +} + +/** + * Move an object within the custom order of a page state + * @param state The current state + * @param action The move action to perform + */ +function moveFieldUpdate(state: any, action: MoveFieldUpdateAction) { + const url = action.payload.url; + const fromIndex = action.payload.from; + const toIndex = action.payload.to; + const fromPage = action.payload.fromPage; + const toPage = action.payload.toPage; + const field = action.payload.field; + + const pageState: ObjectUpdatesEntry = state[url]; + const initialOrderPages = pageState.customOrder.initialOrderPages; + const customOrderPages = [...pageState.customOrder.newOrderPages]; + + // Create a copy of the custom orders for the from- and to-pages + const fromPageOrder = [...customOrderPages[fromPage].order]; + const toPageOrder = [...customOrderPages[toPage].order]; + if (fromPage === toPage) { + if (isNotEmpty(customOrderPages[fromPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex]) && isNotEmpty(customOrderPages[fromPage].order[toIndex])) { + // Move an item from one index to another within the same page + moveItemInArray(fromPageOrder, fromIndex, toIndex); + // Update the custom order for this page + customOrderPages[fromPage] = { order: fromPageOrder }; + } + } else { + if (isNotEmpty(customOrderPages[fromPage]) && hasValue(customOrderPages[toPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex])) { + // Move an item from one index of one page to an index in another page + transferArrayItem(fromPageOrder, toPageOrder, fromIndex, toIndex); + // Update the custom order for both pages + customOrderPages[fromPage] = { order: fromPageOrder }; + customOrderPages[toPage] = { order: toPageOrder }; + } + } + + // Create a field update if it doesn't exist for this field yet + let fieldUpdate = {}; + if (hasValue(field)) { + fieldUpdate = pageState.fieldUpdates[field.uuid]; + if (hasNoValue(fieldUpdate)) { + fieldUpdate = { field: field, changeType: undefined } + } + } + + // Update the store's state with new values and return + return Object.assign({}, state, { [url]: Object.assign({}, pageState, { + fieldUpdates: Object.assign({}, pageState.fieldUpdates, hasValue(field) ? { [field.uuid]: fieldUpdate } : {}), + customOrder: Object.assign({}, pageState.customOrder, { newOrderPages: customOrderPages, changed: checkForOrderChanges(initialOrderPages, customOrderPages) }) + })}) +} + +/** + * Compare two lists of OrderPage objects and return whether there's at least one change in the order of objects within + * @param initialOrderPages The initial list of OrderPages + * @param customOrderPages The changed list of OrderPages + */ +function checkForOrderChanges(initialOrderPages: OrderPage[], customOrderPages: OrderPage[]) { + let changed = false; + initialOrderPages.forEach((orderPage: OrderPage, page: number) => { + if (isNotEmpty(orderPage) && isNotEmpty(orderPage.order) && isNotEmpty(customOrderPages[page]) && isNotEmpty(customOrderPages[page].order)) { + orderPage.order.forEach((id: string, index: number) => { + if (id !== customOrderPages[page].order[index]) { + changed = true; + return; + } + }); + if (changed) { + return; + } + } + }); + return changed; +} + +/** + * Initialize a custom order page by providing the list of all pages, a list of UUIDs, pageSize and the page to populate + * @param initialPages The initial list of OrderPage objects + * @param order The list of UUIDs to create a page for + * @param pageSize The pageSize used to populate empty spacer pages + * @param page The index of the page to add + */ +function addOrderToPages(initialPages: OrderPage[], order: string[], pageSize: number, page: number): OrderPage[] { + const result = [...initialPages]; + const orderPage: OrderPage = { order: order }; + if (page < result.length) { + // The page we're trying to add already exists in the list. Overwrite it. + result[page] = orderPage; + } else if (page === result.length) { + // The page we're trying to add is the next page in the list, add it. + result.push(orderPage); + } else { + // The page we're trying to add is at least one page ahead of the list, fill the list with empty pages before adding the page. + const emptyOrder = []; + for (let i = 0; i < pageSize; i++) { + emptyOrder.push(undefined); + } + const emptyOrderPage: OrderPage = { order: emptyOrder }; + for (let i = result.length; i < page; i++) { + result.push(emptyOrderPage); + } + result.push(orderPage); + } + return result; +} diff --git a/src/app/core/data/object-updates/object-updates.service.spec.ts b/src/app/core/data/object-updates/object-updates.service.spec.ts index 730ee5ad43..780a402a84 100644 --- a/src/app/core/data/object-updates/object-updates.service.spec.ts +++ b/src/app/core/data/object-updates/object-updates.service.spec.ts @@ -2,6 +2,7 @@ import { Store } from '@ngrx/store'; import { CoreState } from '../../core.reducers'; import { ObjectUpdatesService } from './object-updates.service'; import { + AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, @@ -12,6 +13,8 @@ import { Notification } from '../../../shared/notifications/models/notification. import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer'; import {Relationship} from '../../shared/item-relationships/relationship.model'; +import { MoveOperation } from 'fast-json-patch/lib/core'; +import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service'; describe('ObjectUpdatesService', () => { let service: ObjectUpdatesService; @@ -44,7 +47,7 @@ describe('ObjectUpdatesService', () => { }; store = new Store(undefined, undefined, undefined); spyOn(store, 'dispatch'); - service = (new ObjectUpdatesService(store)); + service = new ObjectUpdatesService(store, new ArrayMoveChangeAnalyzer()); spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry)); spyOn(service as any, 'getFieldState').and.callFake((uuid) => { @@ -60,6 +63,25 @@ describe('ObjectUpdatesService', () => { }); }); + describe('initializeWithCustomOrder', () => { + const pageSize = 20; + const page = 0; + + it('should dispatch an INITIALIZE action with the correct URL, initial identifiables, last modified , custom order, page size and page', () => { + service.initializeWithCustomOrder(url, identifiables, modDate, pageSize, page); + expect(store.dispatch).toHaveBeenCalledWith(new InitializeFieldsAction(url, identifiables, modDate, identifiables.map((identifiable) => identifiable.uuid), pageSize, page)); + }); + }); + + describe('addPageToCustomOrder', () => { + const page = 2; + + it('should dispatch an ADD_PAGE_TO_CUSTOM_ORDER action with the correct URL, identifiables, custom order and page number to add', () => { + service.addPageToCustomOrder(url, identifiables, page); + expect(store.dispatch).toHaveBeenCalledWith(new AddPageToCustomOrderAction(url, identifiables, identifiables.map((identifiable) => identifiable.uuid), page)); + }); + }); + describe('getFieldUpdates', () => { it('should return the list of all fields, including their update if there is one', () => { const result$ = service.getFieldUpdates(url, identifiables); @@ -77,6 +99,66 @@ describe('ObjectUpdatesService', () => { }); }); + describe('getFieldUpdatesExclusive', () => { + it('should return the list of all fields, including their update if there is one, excluding updates that aren\'t part of the initial values provided', (done) => { + const result$ = service.getFieldUpdatesExclusive(url, identifiables); + expect((service as any).getObjectEntry).toHaveBeenCalledWith(url); + + const expectedResult = { + [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }, + [identifiable2.uuid]: { field: identifiable2, changeType: undefined } + }; + + result$.subscribe((result) => { + expect(result).toEqual(expectedResult); + done(); + }); + }); + }); + + describe('getFieldUpdatesByCustomOrder', () => { + beforeEach(() => { + const fieldStates = { + [identifiable1.uuid]: { editable: false, isNew: false, isValid: true }, + [identifiable2.uuid]: { editable: true, isNew: false, isValid: false }, + [identifiable3.uuid]: { editable: true, isNew: true, isValid: true }, + }; + + const customOrder = { + initialOrderPages: [{ + order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] + }], + newOrderPages: [{ + order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid] + }], + pageSize: 20, + changed: true + }; + + const objectEntry = { + fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder + }; + + (service as any).getObjectEntry.and.returnValue(observableOf(objectEntry)) + }); + + it('should return the list of all fields, including their update if there is one, ordered by their custom order', (done) => { + const result$ = service.getFieldUpdatesByCustomOrder(url, identifiables); + expect((service as any).getObjectEntry).toHaveBeenCalledWith(url); + + const expectedResult = { + [identifiable2.uuid]: { field: identifiable2, changeType: undefined }, + [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD }, + [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE } + }; + + result$.subscribe((result) => { + expect(result).toEqual(expectedResult); + done(); + }); + }); + }); + describe('isEditable', () => { it('should return false if this identifiable is currently not editable in the store', () => { const result$ = service.isEditable(url, identifiable1.uuid); @@ -192,7 +274,11 @@ describe('ObjectUpdatesService', () => { }); describe('when updates are emtpy', () => { beforeEach(() => { - (service as any).getObjectEntry.and.returnValue(observableOf({})) + (service as any).getObjectEntry.and.returnValue(observableOf({ + customOrder: { + changed: false + } + })) }); it('should return false when there are no updates', () => { @@ -259,4 +345,45 @@ describe('ObjectUpdatesService', () => { expect(store.dispatch).toHaveBeenCalledWith(new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true)); }); }); + + describe('getMoveOperations', () => { + beforeEach(() => { + const fieldStates = { + [identifiable1.uuid]: { editable: false, isNew: false, isValid: true }, + [identifiable2.uuid]: { editable: true, isNew: false, isValid: false }, + [identifiable3.uuid]: { editable: true, isNew: true, isValid: true }, + }; + + const customOrder = { + initialOrderPages: [{ + order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] + }], + newOrderPages: [{ + order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid] + }], + pageSize: 20, + changed: true + }; + + const objectEntry = { + fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder + }; + + (service as any).getObjectEntry.and.returnValue(observableOf(objectEntry)) + }); + + it('should return the expected move operations', (done) => { + const result$ = service.getMoveOperations(url); + + const expectedResult = [ + { op: 'move', from: '/0', path: '/2' } + ] as MoveOperation[]; + + result$.subscribe((result) => { + expect(result).toEqual(expectedResult); + done(); + }); + }); + }); + }); diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index 367b73ee30..c9a7f47e81 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -8,15 +8,16 @@ import { Identifiable, OBJECT_UPDATES_TRASH_PATH, ObjectUpdatesEntry, - ObjectUpdatesState, + ObjectUpdatesState, OrderPage, VirtualMetadataSource } from './object-updates.reducer'; import { Observable } from 'rxjs'; import { - AddFieldUpdateAction, + AddFieldUpdateAction, AddPageToCustomOrderAction, DiscardObjectUpdatesAction, FieldChangeType, InitializeFieldsAction, + MoveFieldUpdateAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, @@ -26,6 +27,9 @@ import { import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { INotification } from '../../../shared/notifications/models/notification.model'; +import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service'; +import { MoveOperation } from 'fast-json-patch/lib/core'; +import { flatten } from '@angular/compiler'; function objectUpdatesStateSelector(): MemoizedSelector { return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']); @@ -48,7 +52,8 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel */ @Injectable() export class ObjectUpdatesService { - constructor(private store: Store) { + constructor(private store: Store, + private comparator: ArrayMoveChangeAnalyzer) { } @@ -62,6 +67,28 @@ export class ObjectUpdatesService { this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified)); } + /** + * Method to dispatch an InitializeFieldsAction to the store and keeping track of the order objects are stored + * @param url The page's URL for which the changes are being mapped + * @param fields The initial fields for the page's object + * @param lastModified The date the object was last modified + * @param pageSize The page size to use for adding pages to the custom order + * @param page The first page to populate the custom order with + */ + initializeWithCustomOrder(url, fields: Identifiable[], lastModified: Date, pageSize = 9999, page = 0): void { + this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, fields.map((field) => field.uuid), pageSize, page)); + } + + /** + * Method to dispatch an AddPageToCustomOrderAction, adding a new page to an already existing custom order tracking + * @param url The URL for which the changes are being mapped + * @param fields The fields to add a new page for + * @param page The page number (starting from index 0) + */ + addPageToCustomOrder(url, fields: Identifiable[], page: number): void { + this.store.dispatch(new AddPageToCustomOrderAction(url, fields, fields.map((field) => field.uuid), page)); + } + /** * Method to dispatch an AddFieldUpdateAction to the store * @param url The page's URL for which the changes are saved @@ -94,14 +121,15 @@ export class ObjectUpdatesService { * a FieldUpdates object * @param url The URL of the page for which the FieldUpdates should be requested * @param initialFields The initial values of the fields + * @param ignoreStates Ignore the fieldStates to loop over the fieldUpdates instead */ - getFieldUpdates(url: string, initialFields: Identifiable[]): Observable { + getFieldUpdates(url: string, initialFields: Identifiable[], ignoreStates?: boolean): Observable { const objectUpdates = this.getObjectEntry(url); return objectUpdates.pipe( switchMap((objectEntry) => { const fieldUpdates: FieldUpdates = {}; if (hasValue(objectEntry)) { - Object.keys(objectEntry.fieldStates).forEach((uuid) => { + Object.keys(ignoreStates ? objectEntry.fieldUpdates : objectEntry.fieldStates).forEach((uuid) => { fieldUpdates[uuid] = objectEntry.fieldUpdates[uuid]; }); } @@ -138,6 +166,31 @@ export class ObjectUpdatesService { })) } + /** + * Method that combines the state's updates with the initial values (when there's no update), + * sorted by their custom order to create a FieldUpdates object + * @param url The URL of the page for which the FieldUpdates should be requested + * @param initialFields The initial values of the fields + * @param page The page to retrieve + */ + getFieldUpdatesByCustomOrder(url: string, initialFields: Identifiable[], page = 0): Observable { + const objectUpdates = this.getObjectEntry(url); + return objectUpdates.pipe(map((objectEntry) => { + const fieldUpdates: FieldUpdates = {}; + if (hasValue(objectEntry) && hasValue(objectEntry.customOrder) && isNotEmpty(objectEntry.customOrder.newOrderPages) && page < objectEntry.customOrder.newOrderPages.length) { + for (const uuid of objectEntry.customOrder.newOrderPages[page].order) { + let fieldUpdate = objectEntry.fieldUpdates[uuid]; + if (isEmpty(fieldUpdate)) { + const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid); + fieldUpdate = {field: identifiable, changeType: undefined}; + } + fieldUpdates[uuid] = fieldUpdate; + } + } + return fieldUpdates; + })) + } + /** * Method to check if a specific field is currently editable in the store * @param url The URL of the page on which the field resides @@ -207,6 +260,19 @@ export class ObjectUpdatesService { this.saveFieldUpdate(url, field, FieldChangeType.UPDATE); } + /** + * Dispatches a MoveFieldUpdateAction + * @param url The page's URL for which the changes are saved + * @param from The index of the object to move + * @param to The index to move the object to + * @param fromPage The page to move the object from + * @param toPage The page to move the object to + * @param field Optional field to add to the fieldUpdates list (useful if we want to track updates across multiple pages) + */ + saveMoveFieldUpdate(url: string, from: number, to: number, fromPage = 0, toPage = 0, field?: Identifiable) { + this.store.dispatch(new MoveFieldUpdateAction(url, from, to, fromPage, toPage, field)); + } + /** * Check whether the virtual metadata of a given item is selected to be saved as real metadata * @param url The URL of the page on which the field resides @@ -264,6 +330,15 @@ export class ObjectUpdatesService { this.store.dispatch(new DiscardObjectUpdatesAction(url, undoNotification)); } + /** + * Method to dispatch a DiscardObjectUpdatesAction to the store with discardAll set to true + * @param url The page's URL for which the changes should be discarded + * @param undoNotification The notification which is should possibly be canceled + */ + discardAllFieldUpdates(url: string, undoNotification: INotification) { + this.store.dispatch(new DiscardObjectUpdatesAction(url, undoNotification, true)); + } + /** * Method to dispatch an ReinstateObjectUpdatesAction to the store * @param url The page's URL for which the changes should be reinstated @@ -312,7 +387,7 @@ export class ObjectUpdatesService { * @param url The page's url to check for in the store */ hasUpdates(url: string): Observable { - return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && isNotEmpty(objectEntry.fieldUpdates))); + return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && (isNotEmpty(objectEntry.fieldUpdates) || objectEntry.customOrder.changed))); } /** @@ -330,4 +405,19 @@ export class ObjectUpdatesService { getLastModified(url: string): Observable { return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified)); } + + /** + * Get move operations based on the custom order + * @param url The page's url + */ + getMoveOperations(url: string): Observable { + return this.getObjectEntry(url).pipe( + map((objectEntry) => objectEntry.customOrder), + map((customOrder) => this.comparator.diff( + flatten(customOrder.initialOrderPages.map((orderPage: OrderPage) => orderPage.order)), + flatten(customOrder.newOrderPages.map((orderPage: OrderPage) => orderPage.order))) + ) + ); + } + } diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 2789f6df54..253577a701 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -22,6 +22,7 @@ import { } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; +import { parseJsonSchemaToCommandDescription } from '@angular/cli/utilities/json-schema'; describe('RequestService', () => { let scheduler: TestScheduler; @@ -139,13 +140,21 @@ describe('RequestService', () => { describe('getByUUID', () => { describe('if the request with the specified UUID exists in the store', () => { beforeEach(() => { + let callCounter = 0; + const responses = [ + cold('a', { // A direct hit in the request cache + a: { + completed: true + } + }), + cold('b', { b: undefined }), // No hit in the index + cold('c', { c: undefined }) // So no mapped hit in the request cache + ]; selectSpy.and.callFake(() => { return () => { - return () => hot('a', { - a: { - completed: true - } - }); + const response = responses[callCounter]; + callCounter++; + return () => response; }; }); }); @@ -162,11 +171,19 @@ describe('RequestService', () => { }); }); - describe('if the request with the specified UUID doesn\'t exist in the store', () => { + describe(`if the request with the specified UUID doesn't exist in the store `, () => { beforeEach(() => { + let callCounter = 0; + const responses = [ + cold('a', { a: undefined }), // No direct hit in the request cache + cold('b', { b: undefined }), // No hit in the index + cold('c', { c: undefined }), // So no mapped hit in the request cache + ]; selectSpy.and.callFake(() => { return () => { - return () => hot('a', { a: undefined }); + const response = responses[callCounter]; + callCounter++; + return () => response; }; }); }); @@ -174,11 +191,43 @@ describe('RequestService', () => { it('should return an Observable of undefined', () => { const result = service.getByUUID(testUUID); - scheduler.expectObservable(result).toBe('b', { b: undefined }); + scheduler.expectObservable(result).toBe('a', { a: undefined }); }); }); - }); + describe(`if the request with the specified UUID wasn't sent, because it was already cached`, () => { + beforeEach(() => { + let callCounter = 0; + const responses = [ + cold('a', { a: undefined }), // No direct hit in the request cache with that UUID + cold('b', { b: 'otherRequestUUID' }), // A hit in the index, which returns the uuid of the cached request + cold('c', { // the call to retrieve the cached request using the UUID from the index + c: { + completed: true + } + }) + ]; + selectSpy.and.callFake(() => { + return () => { + const response = responses[callCounter]; + callCounter++; + return () => response; + }; + }); + }); + + it(`it should return the cached request`, () => { + const result = service.getByUUID(testUUID); + + scheduler.expectObservable(result).toBe('c', { + c: { + completed: true + } + }); + }); + }); + + }); describe('getByHref', () => { describe('when the request with the specified href exists in the store', () => { diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 810b0721ae..105d84cf4a 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -2,10 +2,10 @@ import { Injectable } from '@angular/core'; import { HttpHeaders } from '@angular/common/http'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; -import { Observable, race as observableRace } from 'rxjs'; -import { filter, map, mergeMap, take } from 'rxjs/operators'; +import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; +import { filter, map, mergeMap, take, switchMap, startWith } from 'rxjs/operators'; import { cloneDeep, remove } from 'lodash'; -import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, hasValueOperator } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; @@ -110,15 +110,19 @@ export class RequestService { * Retrieve a RequestEntry based on their uuid */ getByUUID(uuid: string): Observable { - return observableRace( - this.store.pipe(select(entryFromUUIDSelector(uuid))), + return observableCombineLatest([ + this.store.pipe( + select(entryFromUUIDSelector(uuid)) + ), this.store.pipe( select(originalRequestUUIDFromRequestUUIDSelector(uuid)), - mergeMap((originalUUID) => { - return this.store.pipe(select(entryFromUUIDSelector(originalUUID))) + switchMap((originalUUID) => { + return this.store.pipe(select(entryFromUUIDSelector(originalUUID))) }, - )) - ).pipe( + ), + ), + ]).pipe( + map((entries: RequestEntry[]) => entries.find((entry: RequestEntry) => hasValue(entry))), map((entry: RequestEntry) => { // Headers break after being retrieved from the store (because of lazy initialization) // Combining them with a new object fixes this issue @@ -137,7 +141,13 @@ export class RequestService { getByHref(href: string): Observable { return this.store.pipe( select(uuidFromHrefSelector(href)), - mergeMap((uuid: string) => this.getByUUID(uuid)) + mergeMap((uuid: string) => { + if (isNotEmpty(uuid)) { + return this.getByUUID(uuid); + } else { + return [undefined]; + } + }) ); } diff --git a/src/app/core/data/workflow-action-data.service.ts b/src/app/core/data/workflow-action-data.service.ts new file mode 100644 index 0000000000..be2a170ac5 --- /dev/null +++ b/src/app/core/data/workflow-action-data.service.ts @@ -0,0 +1,41 @@ +import { DataService } from './data.service'; +import { WorkflowAction } from '../tasks/models/workflow-action-object.model'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { FindListOptions } from './request.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { Injectable } from '@angular/core'; +import { dataService } from '../cache/builders/build-decorators'; +import { WORKFLOW_ACTION } from '../tasks/models/workflow-action-object.resource-type'; + +/** + * A service responsible for fetching/sending data from/to the REST API on the workflowactions endpoint + */ +@Injectable() +@dataService(WORKFLOW_ACTION) +export class WorkflowActionDataService extends DataService { + protected linkPath = 'workflowactions'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } +} diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index b60816c050..d83a376da9 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -10,7 +10,6 @@ import { Observable } from 'rxjs/internal/Observable'; import { TestScheduler } from 'rxjs/testing'; import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions'; import { SearchParam } from '../cache/models/search-param.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { ChangeAnalyzer } from '../data/change-analyzer'; import { PaginatedList } from '../data/paginated-list'; @@ -36,43 +35,15 @@ describe('EPersonDataService', () => { let requestService: RequestService; let scheduler: TestScheduler; - const epeople = [EPersonMock, EPersonMock2]; + let epeople; - const restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; - const epersonsEndpoint = `${restEndpointURL}/epersons`; - let halService: any = new HALEndpointServiceStub(restEndpointURL); - const epeople$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [epeople])); - const rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ }); - const objectCache = Object.assign({ - /* tslint:disable:no-empty */ - remove: () => { - }, - hasBySelfLinkObservable: () => observableOf(false) - /* tslint:enable:no-empty */ - }) as ObjectCacheService; + let restEndpointURL; + let epersonsEndpoint; + let halService: any; + let epeople$; + let rdbService; - TestBed.configureTestingModule({ - imports: [ - CommonModule, - StoreModule.forRoot({}), - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), - ], - declarations: [], - providers: [], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }); - - const getRequestEntry$ = (successful: boolean) => { - return observableOf({ - completed: true, - response: { isSuccessful: successful, payload: epeople } as any - } as RequestEntry) - }; + let getRequestEntry$; function initTestService() { return new EPersonDataService( @@ -87,7 +58,39 @@ describe('EPersonDataService', () => { ); } + function init() { + getRequestEntry$ = (successful: boolean) => { + return observableOf({ + completed: true, + response: { isSuccessful: successful, payload: epeople } as any + } as RequestEntry) + }; + restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; + epersonsEndpoint = `${restEndpointURL}/epersons`; + epeople = [EPersonMock, EPersonMock2]; + epeople$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [epeople])); + rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ }); + halService = new HALEndpointServiceStub(restEndpointURL); + + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot({}), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + } + beforeEach(() => { + init(); requestService = getMockRequestService(getRequestEntry$(true)); store = new Store(undefined, undefined, undefined); service = initTestService(); diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index ec8b96d1cd..a8cee6f1de 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -181,7 +181,7 @@ export class EPersonDataService extends DataService { } /** - * Method that clears a cached EPerson request and returns its REST url + * Method that clears a cached EPerson request */ public clearEPersonRequests(): void { this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => { @@ -189,6 +189,13 @@ export class EPersonDataService extends DataService { }); } + /** + * Method that clears a link's requests in cache + */ + public clearLinkRequests(href: string): void { + this.requestService.removeByHrefSubstring(href); + } + /** * Method to retrieve the eperson that is currently being edited */ @@ -219,4 +226,27 @@ export class EPersonDataService extends DataService { return this.delete(ePerson.id); } + /** + * Change which ePerson is being edited and return the link for EPeople edit page + * @param ePerson New EPerson to edit + */ + public startEditingNewEPerson(ePerson: EPerson): string { + this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => { + if (ePerson === activeEPerson) { + this.cancelEditEPerson(); + } else { + this.editEPerson(ePerson); + } + }); + return '/admin/access-control/epeople'; + } + + /** + * Get EPeople admin page + * @param ePerson New EPerson to edit + */ + public getEPeoplePageRouterLink(): string { + return '/admin/access-control/epeople'; + } + } diff --git a/src/app/core/eperson/group-data.service.spec.ts b/src/app/core/eperson/group-data.service.spec.ts new file mode 100644 index 0000000000..138cf547f2 --- /dev/null +++ b/src/app/core/eperson/group-data.service.spec.ts @@ -0,0 +1,198 @@ +import { CommonModule } from '@angular/common'; +import { HttpHeaders } from '@angular/common/http'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Store, StoreModule } from '@ngrx/store'; +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { compare, Operation } from 'fast-json-patch'; +import { + GroupRegistryCancelGroupAction, + GroupRegistryEditGroupAction +} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; +import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/mock-remote-data-build.service'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; +import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson-mock'; +import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { SearchParam } from '../cache/models/search-param.model'; +import { CoreState } from '../core.reducers'; +import { ChangeAnalyzer } from '../data/change-analyzer'; +import { PaginatedList } from '../data/paginated-list'; +import { DeleteByIDRequest, DeleteRequest, FindListOptions, PostRequest } from '../data/request.models'; +import { RequestEntry } from '../data/request.reducer'; +import { RequestService } from '../data/request.service'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { Item } from '../shared/item.model'; +import { PageInfo } from '../shared/page-info.model'; +import { GroupDataService } from './group-data.service'; + +describe('GroupDataService', () => { + let service: GroupDataService; + let store: Store; + let requestService: RequestService; + + let restEndpointURL; + let groupsEndpoint; + let groups; + let groups$; + let halService; + let rdbService; + + let getRequestEntry$; + + function init() { + getRequestEntry$ = (successful: boolean) => { + return observableOf({ + completed: true, + response: { isSuccessful: successful, payload: groups } as any + } as RequestEntry) + }; + restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; + groupsEndpoint = `${restEndpointURL}/groups`; + groups = [GroupMock, GroupMock2]; + groups$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), groups)); + rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ }); + halService = new HALEndpointServiceStub(restEndpointURL); + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot({}), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + declarations: [], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + } + + function initTestService() { + return new GroupDataService( + new DummyChangeAnalyzer() as any, + null, + null, + requestService, + rdbService, + store, + null, + halService, + null, + ); + }; + + beforeEach(() => { + init(); + requestService = getMockRequestService(getRequestEntry$(true)); + store = new Store(undefined, undefined, undefined); + service = initTestService(); + spyOn(store, 'dispatch'); + }); + + describe('searchGroups', () => { + beforeEach(() => { + spyOn(service, 'searchBy'); + }); + + it('search with empty query', () => { + service.searchGroups(''); + const options = Object.assign(new FindListOptions(), { + searchParams: [Object.assign(new SearchParam('query', ''))] + }); + expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); + }); + + it('search with query', () => { + service.searchGroups('test'); + const options = Object.assign(new FindListOptions(), { + searchParams: [Object.assign(new SearchParam('query', 'test'))] + }); + expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); + }); + }); + + describe('deleteGroup', () => { + beforeEach(() => { + service.deleteGroup(GroupMock2).subscribe(); + }); + + it('should send DeleteRequest', () => { + const expected = new DeleteByIDRequest(requestService.generateRequestId(), groupsEndpoint + '/' + GroupMock2.uuid, GroupMock2.uuid); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('addSubGroupToGroup', () => { + beforeEach(() => { + service.addSubGroupToGroup(GroupMock, GroupMock2).subscribe(); + }); + it('should send PostRequest to eperson/groups/group-id/subgroups endpoint with new subgroup link in body', () => { + let headers = new HttpHeaders(); + const options: HttpOptions = Object.create({}); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('deleteSubGroupFromGroup', () => { + beforeEach(() => { + service.deleteSubGroupFromGroup(GroupMock, GroupMock2).subscribe(); + }); + it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => { + const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('addMemberToGroup', () => { + beforeEach(() => { + service.addMemberToGroup(GroupMock, EPersonMock2).subscribe(); + }); + it('should send PostRequest to eperson/groups/group-id/epersons endpoint with new eperson member in body', () => { + let headers = new HttpHeaders(); + const options: HttpOptions = Object.create({}); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('deleteMemberFromGroup', () => { + beforeEach(() => { + service.deleteMemberFromGroup(GroupMock, EPersonMock).subscribe(); + }); + it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => { + const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('editGroup', () => { + it('should dispatch a EDIT_GROUP action with the groupp to start editing', () => { + service.editGroup(GroupMock); + expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryEditGroupAction(GroupMock)); + }); + }); + + describe('cancelEditGroup', () => { + it('should dispatch a CANCEL_EDIT_GROUP action', () => { + service.cancelEditGroup(); + expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryCancelGroupAction()); + }); + }); +}); + +class DummyChangeAnalyzer implements ChangeAnalyzer { + diff(object1: Item, object2: Item): Operation[] { + return compare((object1 as any).metadata, (object2 as any).metadata); + } +} diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 532f42323a..574b4d997a 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -1,28 +1,52 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Store } from '@ngrx/store'; +import { createSelector, select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { filter, map, take } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators'; +import { + GroupRegistryCancelGroupAction, + GroupRegistryEditGroupAction +} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; +import { GroupRegistryState } from '../../+admin/admin-access-control/group-registry/group-registry.reducers'; +import { AppState } from '../../app.reducer'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { SearchParam } from '../cache/models/search-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; import { DataService } from '../data/data.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { PaginatedList } from '../data/paginated-list'; +import { RemoteData } from '../data/remote-data'; +import { + CreateRequest, + DeleteRequest, + FindListOptions, + FindListRequest, + PostRequest +} from '../data/request.models'; import { RequestService } from '../data/request.service'; -import { FindListOptions } from '../data/request.models'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { configureRequest, getResponseFromEntry} from '../shared/operators'; +import { EPerson } from './models/eperson.model'; import { Group } from './models/group.model'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { CoreState } from '../core.reducers'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { SearchParam } from '../cache/models/search-param.model'; -import { RemoteData } from '../data/remote-data'; -import { PaginatedList } from '../data/paginated-list'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { dataService } from '../cache/builders/build-decorators'; import { GROUP } from './models/group.resource-type'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; +import { Community } from '../shared/community.model'; +import { Collection } from '../shared/collection.model'; +import { ComcolRole } from '../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role'; + +const groupRegistryStateSelector = (state: AppState) => state.groupRegistry; +const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup); /** - * Provides methods to retrieve eperson group resources. + * Provides methods to retrieve eperson group resources from the REST API & Group related CRUD actions. */ @Injectable({ providedIn: 'root' @@ -31,6 +55,8 @@ import { GROUP } from './models/group.resource-type'; export class GroupDataService extends DataService { protected linkPath = 'groups'; protected browseEndpoint = ''; + public ePersonsEndpoint = 'epersons'; + public subgroupsEndpoint = 'subgroups'; constructor( protected comparator: DSOChangeAnalyzer, @@ -38,13 +64,52 @@ export class GroupDataService extends DataService { protected notificationsService: NotificationsService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected store: Store, + protected store: Store, protected objectCache: ObjectCacheService, - protected halService: HALEndpointService + protected halService: HALEndpointService, + protected nameService: DSONameService, ) { super(); } + /** + * Retrieves all groups + * @param pagination The pagination info used to retrieve the groups + */ + public getGroups(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + const hrefObs = this.getFindAllHref(options, this.linkPath, ...linksToFollow); + hrefObs.pipe( + filter((href: string) => hasValue(href)), + take(1)) + .subscribe((href: string) => { + const request = new FindListRequest(this.requestService.generateRequestId(), href, options); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(hrefObs) as Observable>>; + } + + /** + * Returns a search result list of groups, with certain query (searches in group name and by exact uuid) + * Endpoint used: /eperson/groups/search/byMetadata?query=<:name> + * @param query search query param + * @param options + * @param linksToFollow + */ + public searchGroups(query: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + const searchParams = [new SearchParam('query', query)]; + let findListOptions = new FindListOptions(); + if (options) { + findListOptions = Object.assign(new FindListOptions(), options); + } + if (findListOptions.searchParams) { + findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams]; + } else { + findListOptions.searchParams = searchParams; + } + return this.searchBy('byMetadata', findListOptions, ...linksToFollow); + } + /** * Check if the current user is member of to the indicated group * @@ -59,10 +124,252 @@ export class GroupDataService extends DataService { options.searchParams = [new SearchParam('groupName', groupName)]; return this.searchBy(searchHref, options).pipe( - filter((groups: RemoteData>) => !groups.isResponsePending), - take(1), - map((groups: RemoteData>) => groups.payload.totalElements > 0) - ); + filter((groups: RemoteData>) => !groups.isResponsePending), + take(1), + map((groups: RemoteData>) => groups.payload.totalElements > 0) + ); } + /** + * Method to delete a group + * @param id The group id to delete + */ + public deleteGroup(group: Group): Observable { + return this.delete(group.id); + } + + /** + * Create or Update a group + * If the group contains an id, it is assumed the eperson already exists and is updated instead + * @param group The group to create or update + */ + public createOrUpdateGroup(group: Group): Observable> { + const isUpdate = hasValue(group.id); + if (isUpdate) { + return this.updateGroup(group); + } else { + return this.create(group, null); + } + } + + /** + * // TODO + * @param {DSpaceObject} ePerson The given object + */ + updateGroup(group: Group): Observable> { + // TODO + return null; + } + + /** + * Adds given subgroup as a subgroup to the given active group + * @param activeGroup Group we want to add subgroup to + * @param subgroup Group we want to add as subgroup to activeGroup + */ + addSubGroupToGroup(activeGroup: Group, subgroup: Group): Observable { + const requestId = this.requestService.generateRequestId(); + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options); + this.requestService.configure(postRequest); + + return this.fetchResponse(requestId); + } + + /** + * Deletes a given subgroup from the subgroups of the given active group + * @param activeGroup Group we want to delete subgroup from + * @param subgroup Subgroup we want to delete from activeGroup + */ + deleteSubGroupFromGroup(activeGroup: Group, subgroup: Group): Observable { + const requestId = this.requestService.generateRequestId(); + const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id); + this.requestService.configure(deleteRequest); + + return this.fetchResponse(requestId); + } + + /** + * Adds given ePerson as member to given group + * @param activeGroup Group we want to add member to + * @param ePerson EPerson we want to add as member to given activeGroup + */ + addMemberToGroup(activeGroup: Group, ePerson: EPerson): Observable { + const requestId = this.requestService.generateRequestId(); + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options); + this.requestService.configure(postRequest); + + return this.fetchResponse(requestId); + } + + /** + * Deletes a given ePerson from the members of the given active group + * @param activeGroup Group we want to delete member from + * @param ePerson EPerson we want to delete from members of given activeGroup + */ + deleteMemberFromGroup(activeGroup: Group, ePerson: EPerson): Observable { + const requestId = this.requestService.generateRequestId(); + const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id); + this.requestService.configure(deleteRequest); + + return this.fetchResponse(requestId); + } + + /** + * Gets the restResponse from the requestService + * @param requestId + */ + protected fetchResponse(requestId: string): Observable { + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + map((response: RestResponse) => { + return response; + }) + ); + } + + /** + * Method to retrieve the group that is currently being edited + */ + public getActiveGroup(): Observable { + return this.store.pipe(select(editGroupSelector)) + } + + /** + * Method to cancel editing a group, dispatches a cancel group action + */ + public cancelEditGroup() { + this.store.dispatch(new GroupRegistryCancelGroupAction()); + } + + /** + * Method to set the group being edited, dispatches an edit group action + * @param group The group to edit + */ + public editGroup(group: Group) { + this.store.dispatch(new GroupRegistryEditGroupAction(group)); + } + + /** + * Method that clears a cached groups request + */ + public clearGroupsRequests(): void { + this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => { + this.requestService.removeByHrefSubstring(link); + }); + } + + /** + * Method that clears a cached get subgroups of certain group request + */ + public clearGroupLinkRequests(href: string): void { + this.requestService.removeByHrefSubstring(href); + } + + public getGroupRegistryRouterLink(): string { + return '/admin/access-control/groups'; + } + + /** + * Change which group is being edited and return the link for the edit page of the new group being edited + * @param newGroup New group to edit + */ + public startEditingNewGroup(newGroup: Group): string { + this.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (newGroup === activeGroup) { + this.cancelEditGroup() + } else { + this.editGroup(newGroup) + } + }); + return this.getGroupEditPageRouterLinkWithID(newGroup.id) + } + + /** + * Get Edit page of group + * @param group Group we want edit page for + */ + public getGroupEditPageRouterLink(group: Group): string { + return this.getGroupEditPageRouterLinkWithID(group.id); + } + + /** + * Get Edit page of group + * @param groupID Group ID we want edit page for + */ + public getGroupEditPageRouterLinkWithID(groupId: string): string { + return '/admin/access-control/groups/' + groupId; + } + + /** + * Extract optional UUID from a string + * @param stringWithUUID String with possible UUID + */ + public getUUIDFromString(stringWithUUID: string): string { + let foundUUID = ''; + const uuidMatches = stringWithUUID.match(/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/g); + if (uuidMatches != null) { + foundUUID = uuidMatches[0]; + } + return foundUUID; + } + + /** + * Create a group for a given role for a given community or collection. + * + * @param dso The community or collection for which to create a group + * @param link The REST endpoint to create the group + */ + createComcolGroup(dso: Community|Collection, link: string): Observable { + + const requestId = this.requestService.generateRequestId(); + const group = Object.assign(new Group(), { + metadata: { + 'dc.description': [ + { + value: `${this.nameService.getName(dso)} admin group`, + } + ], + }, + }); + + this.requestService.configure( + new CreateRequest( + requestId, + link, + JSON.stringify(group), + )); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + tap(() => this.requestService.removeByHrefSubstring(link)), + ); + } + + /** + * Delete the group for a given role for a given community or collection. + * + * @param link The REST endpoint to delete the group + */ + deleteComcolGroup(link: string): Observable { + + const requestId = this.requestService.generateRequestId(); + + this.requestService.configure( + new DeleteRequest( + requestId, + link, + )); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + tap(() => this.requestService.removeByHrefSubstring(link)), + ); + } } diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index 5d531800b8..e496babddc 100644 --- a/src/app/core/eperson/models/group.model.ts +++ b/src/app/core/eperson/models/group.model.ts @@ -6,6 +6,8 @@ import { RemoteData } from '../../data/remote-data'; import { DSpaceObject } from '../../shared/dspace-object.model'; import { HALLink } from '../../shared/hal-link.model'; +import { EPerson } from './eperson.model'; +import { EPERSON } from './eperson.resource-type'; import { GROUP } from './group.resource-type'; @typedObject @@ -13,6 +15,12 @@ import { GROUP } from './group.resource-type'; export class Group extends DSpaceObject { static type = GROUP; + /** + * A string representing the unique name of this Group + */ + @autoserialize + public name: string; + /** * A string representing the unique handle of this Group */ @@ -31,7 +39,8 @@ export class Group extends DSpaceObject { @deserialize _links: { self: HALLink; - groups: HALLink; + subgroups: HALLink; + epersons: HALLink; }; /** @@ -39,6 +48,13 @@ export class Group extends DSpaceObject { * Will be undefined unless the groups {@link HALLink} has been resolved. */ @link(GROUP, true) - public groups?: Observable>>; + public subgroups?: Observable>>; + + /** + * The list of EPeople in this group + * Will be undefined unless the epersons {@link HALLink} has been resolved. + */ + @link(EPERSON, true) + public epersons?: Observable>>; } diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 231d44eeff..ab9d1548b7 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -54,7 +54,7 @@ export class Bitstream extends DSpaceObject implements HALResource { * The BitstreamFormat of this Bitstream * Will be undefined unless the format {@link HALLink} has been resolved. */ - @link(BITSTREAM_FORMAT) + @link(BITSTREAM_FORMAT, false, 'format') format?: Observable>; } diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index ba2f448bba..4e0b5ead83 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -15,6 +15,8 @@ import { RESOURCE_POLICY } from './resource-policy.resource-type'; import { COMMUNITY } from './community.resource-type'; import { Community } from './community.model'; import { ChildHALResource } from './child-hal-resource.model'; +import { GROUP } from '../eperson/models/group.resource-type'; +import { Group } from '../eperson/models/group.model'; @typedObject @inheritSerialization(DSpaceObject) @@ -70,6 +72,12 @@ export class Collection extends DSpaceObject implements ChildHALResource { @link(COMMUNITY, false) parentCommunity?: Observable>; + /** + * The administrators group of this community. + */ + @link(GROUP) + adminGroup?: Observable>; + /** * The introductory text of this Collection * Corresponds to the metadata field dc.description diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index e18ec743e8..bdcda70e9b 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -3,6 +3,8 @@ import { Observable } from 'rxjs'; import { link, typedObject } from '../cache/builders/build-decorators'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; +import { Group } from '../eperson/models/group.model'; +import { GROUP } from '../eperson/models/group.resource-type'; import { Bitstream } from './bitstream.model'; import { BITSTREAM } from './bitstream.resource-type'; import { Collection } from './collection.model'; @@ -32,6 +34,7 @@ export class Community extends DSpaceObject implements ChildHALResource { logo: HALLink; subcommunities: HALLink; parentCommunity: HALLink; + adminGroup: HALLink; self: HALLink; }; @@ -63,6 +66,12 @@ export class Community extends DSpaceObject implements ChildHALResource { @link(COMMUNITY, false) parentCommunity?: Observable>; + /** + * The administrators group of this community. + */ + @link(GROUP) + adminGroup?: Observable>; + /** * The introductory text of this Community * Corresponds to the metadata field dc.description diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 60a1160d3e..a9256fbb7f 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -1,5 +1,5 @@ import { autoserialize, autoserializeAs, deserialize, deserializeAs } from 'cerialize'; -import { hasNoValue, isUndefined } from '../../shared/empty.util'; +import { hasNoValue, hasValue, isUndefined } from '../../shared/empty.util'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { typedObject } from '../cache/builders/build-decorators'; import { CacheableObject } from '../cache/object-cache.reducer'; @@ -79,6 +79,9 @@ export class DSpaceObject extends ListableObject implements CacheableObject { * The name for this DSpaceObject */ set name(name) { + if (hasValue(this.firstMetadata('dc.title'))) { + this.firstMetadata('dc.title').value = name; + } this._name = name; } diff --git a/src/app/core/shared/metadata.utils.spec.ts b/src/app/core/shared/metadata.utils.spec.ts index f4b3517649..016ef594b1 100644 --- a/src/app/core/shared/metadata.utils.spec.ts +++ b/src/app/core/shared/metadata.utils.spec.ts @@ -7,6 +7,7 @@ import { MetadatumViewModel } from './metadata.models'; import { Metadata } from './metadata.utils'; +import { beforeEach } from 'selenium-webdriver/testing'; const mdValue = (value: string, language?: string, authority?: string): MetadataValue => { return Object.assign(new MetadataValue(), { uuid: uuidv4(), value: value, language: isUndefined(language) ? null : language, place: 0, authority: isUndefined(authority) ? null : authority, confidence: undefined }); @@ -216,4 +217,26 @@ describe('Metadata', () => { testToMetadataMap(multiViewModelList, multiMap); }); + describe('setFirstValue method', () => { + + const metadataMap = { + 'dc.description': [mdValue('Test description')], + 'dc.title': [mdValue('Test title 1'), mdValue('Test title 2')] + }; + + const testSetFirstValue = (map: MetadataMap, key: string, value: string) => { + describe(`with field ${key} and value ${value}`, () => { + Metadata.setFirstValue(map, key, value); + it(`should set first value of ${key} to ${value}`, () => { + expect(map[key][0].value).toEqual(value); + }); + }); + }; + + testSetFirstValue(metadataMap, 'dc.description', 'New Description'); + testSetFirstValue(metadataMap, 'dc.title', 'New Title'); + testSetFirstValue(metadataMap, 'dc.format', 'Completely new field and value'); + + }); + }); diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts index 334c430968..24ff06f4c9 100644 --- a/src/app/core/shared/metadata.utils.ts +++ b/src/app/core/shared/metadata.utils.ts @@ -1,4 +1,4 @@ -import { isEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util'; import { MetadataMapInterface, MetadataValue, @@ -217,4 +217,19 @@ export class Metadata { }); return metadataMap; } + + /** + * Set the first value of a metadata by field key + * Creates a new MetadataValue if the field doesn't exist yet + * @param mdMap The map to add/change values in + * @param key The metadata field + * @param value The value to add + */ + public static setFirstValue(mdMap: MetadataMapInterface, key: string, value: string) { + if (isNotEmpty(mdMap[key])) { + mdMap[key][0].value = value; + } else { + mdMap[key] = [Object.assign(new MetadataValue(), { value: value })] + } + } } diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 14d101a448..a51e711d26 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,6 +1,6 @@ import { Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { filter, find, flatMap, map, tap } from 'rxjs/operators'; +import { filter, find, flatMap, map, take, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { SearchResult } from '../../shared/search/search-result.model'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; @@ -207,3 +207,13 @@ export const getFirstOccurrence = () => source.pipe( map((rd) => Object.assign(rd, { payload: rd.payload.page.length > 0 ? rd.payload.page[0] : undefined })) ); + +/** + * Operator for turning the current page of bitstreams into an array + */ +export const paginatedListToArray = () => + (source: Observable>>): Observable => + source.pipe( + hasValueOperator(), + map((objectRD: RemoteData>) => objectRD.payload.page.filter((object: T) => hasValue(object))) + ); diff --git a/src/app/core/tasks/claimed-task-data.service.spec.ts b/src/app/core/tasks/claimed-task-data.service.spec.ts index ac5f211682..c787d01282 100644 --- a/src/app/core/tasks/claimed-task-data.service.spec.ts +++ b/src/app/core/tasks/claimed-task-data.service.spec.ts @@ -52,8 +52,7 @@ describe('ClaimedTaskDataService', () => { options.headers = headers; }); - describe('approveTask', () => { - + describe('submitTask', () => { it('should call postToEndpoint method', () => { const scopeId = '1234'; const body = { @@ -63,33 +62,13 @@ describe('ClaimedTaskDataService', () => { spyOn(service, 'postToEndpoint'); requestService.uriEncodeBody.and.returnValue(body); - service.approveTask(scopeId); - - expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, body, scopeId, options); - }); - }); - - describe('rejectTask', () => { - - it('should call postToEndpoint method', () => { - const scopeId = '1234'; - const reason = 'test reject'; - const body = { - submit_reject: 'true', - reason - }; - - spyOn(service, 'postToEndpoint'); - requestService.uriEncodeBody.and.returnValue(body); - - service.rejectTask(reason, scopeId); + service.submitTask(scopeId, body); expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, body, scopeId, options); }); }); describe('returnToPoolTask', () => { - it('should call deleteById method', () => { const scopeId = '1234'; diff --git a/src/app/core/tasks/claimed-task-data.service.ts b/src/app/core/tasks/claimed-task-data.service.ts index 0a9de20530..5815dad6e5 100644 --- a/src/app/core/tasks/claimed-task-data.service.ts +++ b/src/app/core/tasks/claimed-task-data.service.ts @@ -35,7 +35,6 @@ export class ClaimedTaskDataService extends TasksService { * * @param {RequestService} requestService * @param {RemoteDataBuildService} rdbService - * @param {NormalizedObjectBuildService} linkService * @param {Store} store * @param {ObjectCacheService} objectCache * @param {HALEndpointService} halService @@ -56,35 +55,16 @@ export class ClaimedTaskDataService extends TasksService { } /** - * Make a request to approve the given task + * Make a request for the given task * * @param scopeId * The task id + * @param body + * The request body * @return {Observable} * Emit the server response */ - public approveTask(scopeId: string): Observable { - const body = { - submit_approve: 'true' - }; - return this.postToEndpoint(this.linkPath, this.requestService.uriEncodeBody(body), scopeId, this.makeHttpOptions()); - } - - /** - * Make a request to reject the given task - * - * @param reason - * The reason of reject - * @param scopeId - * The task id - * @return {Observable} - * Emit the server response - */ - public rejectTask(reason: string, scopeId: string): Observable { - const body = { - submit_reject: 'true', - reason - }; + public submitTask(scopeId: string, body: any): Observable { return this.postToEndpoint(this.linkPath, this.requestService.uriEncodeBody(body), scopeId, this.makeHttpOptions()); } diff --git a/src/app/core/tasks/models/task-object.model.ts b/src/app/core/tasks/models/task-object.model.ts index 4ad6188d46..d90c7a19c0 100644 --- a/src/app/core/tasks/models/task-object.model.ts +++ b/src/app/core/tasks/models/task-object.model.ts @@ -13,6 +13,8 @@ import { HALLink } from '../../shared/hal-link.model'; import { WorkflowItem } from '../../submission/models/workflowitem.model'; import { TASK_OBJECT } from './task-object.resource-type'; import { WORKFLOWITEM } from '../../eperson/models/workflowitem.resource-type'; +import { WORKFLOW_ACTION } from './workflow-action-object.resource-type'; +import { WorkflowAction } from './workflow-action-object.model'; /** * An abstract model class for a TaskObject. @@ -34,12 +36,6 @@ export class TaskObject extends DSpaceObject implements CacheableObject { @autoserialize step: string; - /** - * The task action type - */ - @autoserialize - action: string; - /** * The {@link HALLink}s for this TaskObject */ @@ -49,6 +45,7 @@ export class TaskObject extends DSpaceObject implements CacheableObject { owner: HALLink; group: HALLink; workflowitem: HALLink; + action: HALLink; }; /** @@ -73,4 +70,11 @@ export class TaskObject extends DSpaceObject implements CacheableObject { /* This was changed from 'WorkflowItem | Observable>' to 'any' to prevent issues in templates with async */ workflowitem?: any; + /** + * The task action type + * Will be undefined unless the group {@link HALLink} has been resolved. + */ + @link(WORKFLOW_ACTION, false, 'action') + action: Observable>; + } diff --git a/src/app/core/tasks/models/workflow-action-object.model.ts b/src/app/core/tasks/models/workflow-action-object.model.ts new file mode 100644 index 0000000000..720d817859 --- /dev/null +++ b/src/app/core/tasks/models/workflow-action-object.model.ts @@ -0,0 +1,25 @@ +import { inheritSerialization, autoserialize } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { WORKFLOW_ACTION } from './workflow-action-object.resource-type'; + +/** + * A model class for a WorkflowAction + */ +@typedObject +@inheritSerialization(DSpaceObject) +export class WorkflowAction extends DSpaceObject { + static type = WORKFLOW_ACTION; + + /** + * The workflow action's identifier + */ + @autoserialize + id: string; + + /** + * The options available for this workflow action + */ + @autoserialize + options: string[]; +} diff --git a/src/app/core/tasks/models/workflow-action-object.resource-type.ts b/src/app/core/tasks/models/workflow-action-object.resource-type.ts new file mode 100644 index 0000000000..d48ffd18f4 --- /dev/null +++ b/src/app/core/tasks/models/workflow-action-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for WorkflowAction + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const WORKFLOW_ACTION = new ResourceType('workflowaction'); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html new file mode 100644 index 0000000000..1e89e3facf --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html @@ -0,0 +1,52 @@ +
+ +
+ +
+ +
+ {{'comcol-role.edit.' + comcolRole.name + '.name' | translate}} +
+ +
+ +
+ {{'comcol-role.edit.no-group' | translate}} +
+
+ {{'comcol-role.edit.' + comcolRole.name + '.anonymous-group' | translate}} +
+ + {{group.name}} + +
+ +
+
+ {{'comcol-role.edit.create' | translate}} +
+
+ {{'comcol-role.edit.restrict' | translate}} +
+
+ {{'comcol-role.edit.delete' | translate}} +
+
+ +
+ +
+ {{'comcol-role.edit.' + comcolRole.name + '.description' | translate}} +
+ +
+ +
diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.scss b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts new file mode 100644 index 0000000000..4694c13603 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts @@ -0,0 +1,176 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComcolRoleComponent } from './comcol-role.component'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { By } from '@angular/platform-browser'; +import { SharedModule } from '../../../shared.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { RequestService } from '../../../../core/data/request.service'; +import { ComcolRole } from './comcol-role'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Collection } from '../../../../core/shared/collection.model'; + +describe('ComcolRoleComponent', () => { + + let fixture: ComponentFixture; + let comp: ComcolRoleComponent; + let de: DebugElement; + + let requestService; + let groupService; + + let group; + let statusCode; + + beforeEach(() => { + + requestService = {hasByHrefObservable: () => observableOf(true)}; + + groupService = { + findByHref: () => undefined, + createComcolGroup: jasmine.createSpy('createComcolGroup'), + deleteComcolGroup: jasmine.createSpy('deleteComcolGroup'), + }; + + spyOn(groupService, 'findByHref').and.callFake((link) => { + if (link === 'test role link') { + return observableOf(new RemoteData( + false, + false, + true, + undefined, + group, + statusCode, + )); + } + }); + + TestBed.configureTestingModule({ + imports: [ + SharedModule, + RouterTestingModule.withRoutes([]), + TranslateModule.forRoot(), + ], + providers: [ + { provide: GroupDataService, useValue: groupService }, + { provide: RequestService, useValue: requestService }, + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + + fixture = TestBed.createComponent(ComcolRoleComponent); + comp = fixture.componentInstance; + de = fixture.debugElement; + + comp.comcolRole = new ComcolRole( + 'test role name', + 'test role endpoint', + ); + + comp.dso = Object.assign( + new Collection(), { + _links: { + 'test role endpoint': { + href: 'test role link', + } + } + } + ); + + fixture.detectChanges(); + }); + + describe('when there is no group yet', () => { + + beforeEach(() => { + group = null; + statusCode = 204; + fixture.detectChanges(); + }); + + it('should have a create button but no restrict or delete button', () => { + expect(de.query(By.css('.btn.create'))) + .toBeTruthy(); + expect(de.query(By.css('.btn.restrict'))) + .toBeNull(); + expect(de.query(By.css('.btn.delete'))) + .toBeNull(); + }); + + describe('when the create button is pressed', () => { + + beforeEach(() => { + de.query(By.css('.btn.create')).nativeElement.click(); + }); + + it('should call the groupService create method', () => { + expect(groupService.createComcolGroup).toHaveBeenCalled(); + }); + }); + }); + + describe('when the related group is the Anonymous group', () => { + + beforeEach(() => { + group = { + name: 'Anonymous' + }; + statusCode = 200; + fixture.detectChanges(); + }); + + it('should have a restrict button but no create or delete button', () => { + expect(de.query(By.css('.btn.create'))) + .toBeNull(); + expect(de.query(By.css('.btn.restrict'))) + .toBeTruthy(); + expect(de.query(By.css('.btn.delete'))) + .toBeNull(); + }); + + describe('when the restrict button is pressed', () => { + + beforeEach(() => { + de.query(By.css('.btn.restrict')).nativeElement.click(); + }); + + it('should call the groupService create method', () => { + expect(groupService.createComcolGroup).toHaveBeenCalledWith(comp.dso, 'test role link'); + }); + }); + }); + + describe('when the related group is a custom group', () => { + + beforeEach(() => { + group = { + name: 'custom group name' + }; + statusCode = 200; + fixture.detectChanges(); + }); + + it('should have a delete button but no create or restrict button', () => { + expect(de.query(By.css('.btn.create'))) + .toBeNull(); + expect(de.query(By.css('.btn.restrict'))) + .toBeNull(); + expect(de.query(By.css('.btn.delete'))) + .toBeTruthy(); + }); + + describe('when the delete button is pressed', () => { + + beforeEach(() => { + de.query(By.css('.btn.delete')).nativeElement.click(); + }); + + it('should call the groupService delete method', () => { + expect(groupService.deleteComcolGroup).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts new file mode 100644 index 0000000000..41cb7e7cd2 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts @@ -0,0 +1,126 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Group } from '../../../../core/eperson/models/group.model'; +import { Community } from '../../../../core/shared/community.model'; +import { Observable } from 'rxjs'; +import { getGroupEditPath } from '../../../../+admin/admin-access-control/admin-access-control-routing.module'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { Collection } from '../../../../core/shared/collection.model'; +import { filter, map } from 'rxjs/operators'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { ComcolRole } from './comcol-role'; +import { RequestService } from '../../../../core/data/request.service'; +import { RemoteData } from '../../../../core/data/remote-data'; + +/** + * Component for managing a community or collection role. + */ +@Component({ + selector: 'ds-comcol-role', + styleUrls: ['./comcol-role.component.scss'], + templateUrl: './comcol-role.component.html' +}) +export class ComcolRoleComponent implements OnInit { + + /** + * The community or collection to manage. + */ + @Input() + dso: Community|Collection; + + /** + * The role to manage + */ + @Input() + comcolRole: ComcolRole; + + constructor( + protected requestService: RequestService, + protected groupService: GroupDataService, + ) { + } + + /** + * The link to the related group. + */ + get groupLink(): string { + return this.dso._links[this.comcolRole.linkName].href; + } + + /** + * The group for this role, as an observable remote data. + */ + get groupRD$(): Observable> { + return this.groupService.findByHref(this.groupLink).pipe( + filter((groupRD) => !!groupRD.statusCode), + ); + } + + /** + * The group for this role, as an observable. + */ + get group$(): Observable { + return this.groupRD$.pipe( + getSucceededRemoteData(), + filter((groupRD) => groupRD != null), + getRemoteDataPayload(), + ); + } + + /** + * The link to the group edit page as an observable. + */ + get editGroupLink$(): Observable { + return this.group$.pipe( + map((group) => getGroupEditPath(group.id)), + ); + } + + /** + * Return true if there is no group for this ComcolRole, as an observable. + */ + hasNoGroup$(): Observable { + return this.groupRD$.pipe( + map((groupRD) => groupRD.statusCode === 204), + ) + } + + /** + * Return true if the group for this ComcolRole is the Anonymous group, as an observable. + */ + hasAnonymousGroup$(): Observable { + return this.group$.pipe( + map((group) => group.name === 'Anonymous'), + ) + } + + /** + * Return true if there is a group for this ComcolRole other than the Anonymous group, as an observable. + */ + hasCustomGroup$(): Observable { + return this.hasAnonymousGroup$().pipe( + map((anonymous) => !anonymous), + ) + } + + /** + * Create a group for this community or collection role. + */ + create() { + this.groupService.createComcolGroup(this.dso, this.groupLink).subscribe(); + } + + /** + * Delete the group for this community or collection role. + */ + delete() { + this.groupService.deleteComcolGroup(this.groupLink).subscribe(); + } + + ngOnInit(): void { + this.requestService.hasByHrefObservable(this.groupLink) + .pipe( + filter((hasByHrefObservable) => !hasByHrefObservable), + ) + .subscribe(() => this.groupRD$.subscribe()); + } +} diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts new file mode 100644 index 0000000000..2ac74fe67b --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts @@ -0,0 +1,77 @@ +import { Community } from '../../../../core/shared/community.model'; +import { Collection } from '../../../../core/shared/collection.model'; + +/** + * Class representing a community or collection role. + */ +export class ComcolRole { + + /** + * The community admin role. + */ + public static COMMUNITY_ADMIN = new ComcolRole( + 'community-admin', + 'adminGroup', + ); + + /** + * The collection admin role. + */ + public static COLLECTION_ADMIN = new ComcolRole( + 'collection-admin', + 'adminGroup', + ); + + /** + * The submitters role. + */ + public static SUBMITTERS = new ComcolRole( + 'submitters', + 'submittersGroup', + ); + + /** + * The default item read role. + */ + public static ITEM_READ = new ComcolRole( + 'item_read', + 'itemReadGroup', + ); + + /** + * The default bitstream read role. + */ + public static BITSTREAM_READ = new ComcolRole( + 'bitstream_read', + 'bitstreamReadGroup', + ); + + /** + * @param name The name for this community or collection role. + * @param linkName The path linking to this community or collection role. + */ + constructor( + public name, + public linkName, + ) { + } + + /** + * Get the REST endpoint for managing this role for a given community or collection. + * @param dso + */ + public getEndpoint(dso: Community | Collection) { + + let linkPath; + switch (dso.type + '') { + case 'community': + linkPath = 'communities'; + break; + case 'collection': + linkPath = 'collections'; + break; + } + + return `${linkPath}/${dso.uuid}/${this.linkName}`; + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 4d26f3948d..2089ce8bca 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -76,6 +76,8 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; +import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model'; +import { CustomSwitchComponent } from './models/custom-switch/custom-switch.component'; import { map, startWith, switchMap, find } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { SearchResult } from '../../../search/search-result.model'; @@ -158,6 +160,9 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type< case DYNAMIC_FORM_CONTROL_TYPE_DISABLED: return DsDynamicDisabledComponent; + case DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH: + return CustomSwitchComponent; + default: return null; } @@ -293,6 +298,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } } + get isCheckbox(): boolean { + return this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX || this.model.type === DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH; + } + ngOnChanges(changes: SimpleChanges) { if (changes) { super.ngOnChanges(changes); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html new file mode 100644 index 0000000000..9d059b4bee --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.html @@ -0,0 +1,20 @@ +
+ + +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts new file mode 100644 index 0000000000..6c2502a92b --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts @@ -0,0 +1,99 @@ +import { DynamicFormsCoreModule, DynamicFormService } from '@ng-dynamic-forms/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { DebugElement } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TextMaskModule } from 'angular2-text-mask'; +import { By } from '@angular/platform-browser'; +import { DynamicCustomSwitchModel } from './custom-switch.model'; +import { CustomSwitchComponent } from './custom-switch.component'; + +describe('CustomSwitchComponent', () => { + + const testModel = new DynamicCustomSwitchModel({id: 'switch'}); + const formModel = [testModel]; + let formGroup: FormGroup; + let fixture: ComponentFixture; + let component: CustomSwitchComponent; + let debugElement: DebugElement; + let testElement: DebugElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + NoopAnimationsModule, + TextMaskModule, + DynamicFormsCoreModule.forRoot() + ], + declarations: [CustomSwitchComponent] + + }).compileComponents().then(() => { + fixture = TestBed.createComponent(CustomSwitchComponent); + + component = fixture.componentInstance; + debugElement = fixture.debugElement; + }); + })); + + beforeEach(inject([DynamicFormService], (service: DynamicFormService) => { + formGroup = service.createFormGroup(formModel); + + component.group = formGroup; + component.model = testModel; + + fixture.detectChanges(); + + testElement = debugElement.query(By.css(`input[id='${testModel.id}']`)); + })); + + it('should initialize correctly', () => { + expect(component.bindId).toBe(true); + expect(component.group instanceof FormGroup).toBe(true); + expect(component.model instanceof DynamicCustomSwitchModel).toBe(true); + + expect(component.blur).toBeDefined(); + expect(component.change).toBeDefined(); + expect(component.focus).toBeDefined(); + + expect(component.onBlur).toBeDefined(); + expect(component.onChange).toBeDefined(); + expect(component.onFocus).toBeDefined(); + + expect(component.hasFocus).toBe(false); + expect(component.isValid).toBe(true); + expect(component.isInvalid).toBe(false); + }); + + it('should have an input element', () => { + expect(testElement instanceof DebugElement).toBe(true); + }); + + it('should have an input element of type checkbox', () => { + expect(testElement.nativeElement.getAttribute('type')).toEqual('checkbox'); + }); + + it('should emit blur event', () => { + spyOn(component.blur, 'emit'); + + component.onBlur(null); + + expect(component.blur.emit).toHaveBeenCalled(); + }); + + it('should emit change event', () => { + spyOn(component.change, 'emit'); + + component.onChange(null); + + expect(component.change.emit).toHaveBeenCalled(); + }); + + it('should emit focus event', () => { + spyOn(component.focus, 'emit'); + + component.onFocus(null); + + expect(component.focus.emit).toHaveBeenCalled(); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts new file mode 100644 index 0000000000..ab02fc159d --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts @@ -0,0 +1,55 @@ +import { DynamicNGBootstrapCheckboxComponent } from '@ng-dynamic-forms/ui-ng-bootstrap'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { DynamicCustomSwitchModel } from './custom-switch.model'; + +@Component({ + selector: 'ds-custom-switch', + styleUrls: ['./custom-switch.component.scss'], + templateUrl: './custom-switch.component.html', +}) +/** + * Component displaying a custom switch usable in dynamic forms + * Extends from bootstrap's checkbox component but displays a switch instead + */ +export class CustomSwitchComponent extends DynamicNGBootstrapCheckboxComponent { + /** + * Use the model's ID for the input element + */ + @Input() bindId = true; + + /** + * The formgroup containing this component + */ + @Input() group: FormGroup; + + /** + * The model used for displaying the switch + */ + @Input() model: DynamicCustomSwitchModel; + + /** + * Emit an event when the input is selected + */ + @Output() selected = new EventEmitter(); + + /** + * Emit an event when the input value is removed + */ + @Output() remove = new EventEmitter(); + + /** + * Emit an event when the input is blurred out + */ + @Output() blur = new EventEmitter(); + + /** + * Emit an event when the input value changes + */ + @Output() change = new EventEmitter(); + + /** + * Emit an event when the input is focused + */ + @Output() focus = new EventEmitter(); +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model.ts new file mode 100644 index 0000000000..97cf71c4a0 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model.ts @@ -0,0 +1,20 @@ +import { + DynamicCheckboxModel, + DynamicCheckboxModelConfig, + DynamicFormControlLayout, + serializable +} from '@ng-dynamic-forms/core'; + +export const DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH = 'CUSTOM_SWITCH'; + +/** + * Model class for displaying a custom switch input in a form + * Functions like a checkbox, but displays a switch instead + */ +export class DynamicCustomSwitchModel extends DynamicCheckboxModel { + @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH; + + constructor(config: DynamicCheckboxModelConfig, layout?: DynamicFormControlLayout) { + super(config, layout); + } +} diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 510bf7291b..24948680c7 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -50,9 +50,9 @@
- +
diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 077def0060..def61cb5b2 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -53,6 +53,16 @@ export class FormComponent implements OnDestroy, OnInit { */ @Input() formId: string; + /** + * i18n key for the submit button + */ + @Input() submitLabel = 'form.submit'; + + /** + * i18n key for the cancel button + */ + @Input() cancelLabel = 'form.cancel'; + /** * An array of DynamicFormControlModel type */ diff --git a/src/app/shared/mocks/request.service.mock.ts b/src/app/shared/mocks/request.service.mock.ts index 23101b6feb..da297f56ac 100644 --- a/src/app/shared/mocks/request.service.mock.ts +++ b/src/app/shared/mocks/request.service.mock.ts @@ -11,9 +11,7 @@ export function getMockRequestService(requestEntry$: Observable = getByUUID: requestEntry$, uriEncodeBody: jasmine.createSpy('uriEncodeBody'), isCachedOrPending: false, - hasByHrefObservable: observableOf(false), - /* tslint:disable:no-empty */ - removeByHrefSubstring: () => {} - /* tslint:enable:no-empty */ + removeByHrefSubstring: jasmine.createSpy('removeByHrefSubstring'), + hasByHrefObservable: observableOf(false) }); } diff --git a/src/app/shared/mydspace-actions/claimed-task/abstract/claimed-task-actions-abstract.component.ts b/src/app/shared/mydspace-actions/claimed-task/abstract/claimed-task-actions-abstract.component.ts new file mode 100644 index 0000000000..dafc148147 --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/abstract/claimed-task-actions-abstract.component.ts @@ -0,0 +1,61 @@ +import { EventEmitter, Input, Output } from '@angular/core'; +import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; +import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response'; + +/** + * Abstract component for rendering a claimed task's action + * To create a child-component for a new option: + * - Set the "option" of the component + * - Add a @rendersWorkflowTaskOption annotation to your component providing the same enum value + * - Optionally overwrite createBody if the request body requires more than just the option + */ +export abstract class ClaimedTaskActionsAbstractComponent { + /** + * The workflow task option the child component represents + */ + abstract option: string; + + /** + * The Claimed Task to display an action for + */ + @Input() object: ClaimedTask; + + /** + * Emits the success or failure of a processed action + */ + @Output() processCompleted: EventEmitter = new EventEmitter(); + + /** + * A boolean representing if the operation is pending + */ + processing$ = new BehaviorSubject(false); + + constructor(protected claimedTaskService: ClaimedTaskDataService) { + } + + /** + * Create a request body for submitting the task + * Overwrite this method in the child component if the body requires more than just the option + */ + createbody(): any { + return { + [this.option]: 'true' + }; + } + + /** + * Submit the task for this option + * While the task is submitting, processing$ is set to true and processCompleted emits the response's status when + * completed + */ + submitTask() { + this.processing$.next(true); + this.claimedTaskService.submitTask(this.object.id, this.createbody()) + .subscribe((res: ProcessTaskResponse) => { + this.processing$.next(false); + this.processCompleted.emit(res.hasSucceeded); + }); + } +} diff --git a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.html b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.html index 3c41fdbb07..7944d24d96 100644 --- a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.html +++ b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.html @@ -1,8 +1,8 @@ diff --git a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts index d30c1b7e34..0b276625ba 100644 --- a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts +++ b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts @@ -2,14 +2,23 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; import { ClaimedTaskActionsApproveComponent } from './claimed-task-actions-approve.component'; import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; +import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; +import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response'; let component: ClaimedTaskActionsApproveComponent; let fixture: ComponentFixture; describe('ClaimedTaskActionsApproveComponent', () => { + const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' }); + const claimedTaskService = jasmine.createSpyObj('claimedTaskService', { + submitTask: observableOf(new ProcessTaskResponse(true)) + }); + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -20,6 +29,9 @@ describe('ClaimedTaskActionsApproveComponent', () => { } }) ], + providers: [ + { provide: ClaimedTaskDataService, useValue: claimedTaskService } + ], declarations: [ClaimedTaskActionsApproveComponent], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ClaimedTaskActionsApproveComponent, { @@ -30,14 +42,10 @@ describe('ClaimedTaskActionsApproveComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ClaimedTaskActionsApproveComponent); component = fixture.componentInstance; + component.object = object; fixture.detectChanges(); }); - afterEach(() => { - fixture = null; - component = null; - }); - it('should display approve button', () => { const btn = fixture.debugElement.query(By.css('.btn-success')); @@ -45,7 +53,7 @@ describe('ClaimedTaskActionsApproveComponent', () => { }); it('should display spin icon when approve is pending', () => { - component.processingApprove = true; + component.processing$.next(true); fixture.detectChanges(); const span = fixture.debugElement.query(By.css('.btn-success .fa-spin')); @@ -53,13 +61,27 @@ describe('ClaimedTaskActionsApproveComponent', () => { expect(span).toBeDefined(); }); - it('should emit approve event', () => { - spyOn(component.approve, 'emit'); + describe('submitTask', () => { + let expectedBody; - component.confirmApprove(); - fixture.detectChanges(); + beforeEach(() => { + spyOn(component.processCompleted, 'emit'); - expect(component.approve.emit).toHaveBeenCalled(); + expectedBody = { + [component.option]: 'true' + }; + + component.submitTask(); + fixture.detectChanges(); + }); + + it('should call claimedTaskService\'s submitTask with the expected body', () => { + expect(claimedTaskService.submitTask).toHaveBeenCalledWith(object.id, expectedBody) + }); + + it('should emit a successful processCompleted event', () => { + expect(component.processCompleted.emit).toHaveBeenCalledWith(true); + }); }); }); diff --git a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.ts b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.ts index 8e7c0dab60..8f51ac393c 100644 --- a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.ts +++ b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.ts @@ -1,32 +1,26 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component } from '@angular/core'; +import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component'; +import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; +export const WORKFLOW_TASK_OPTION_APPROVE = 'submit_approve'; + +@rendersWorkflowTaskOption(WORKFLOW_TASK_OPTION_APPROVE) @Component({ selector: 'ds-claimed-task-actions-approve', styleUrls: ['./claimed-task-actions-approve.component.scss'], templateUrl: './claimed-task-actions-approve.component.html', }) - -export class ClaimedTaskActionsApproveComponent { - +/** + * Component for displaying and processing the approve action on a workflow task item + */ +export class ClaimedTaskActionsApproveComponent extends ClaimedTaskActionsAbstractComponent { /** - * A boolean representing if a reject operation is pending + * This component represents the approve option */ - @Input() processingApprove: boolean; + option = WORKFLOW_TASK_OPTION_APPROVE; - /** - * CSS classes to append to reject button - */ - @Input() wrapperClass: string; - - /** - * An event fired when a approve action is confirmed. - */ - @Output() approve: EventEmitter = new EventEmitter(); - - /** - * Emit approve event - */ - confirmApprove() { - this.approve.emit(); + constructor(protected claimedTaskService: ClaimedTaskDataService) { + super(claimedTaskService); } } diff --git a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html index df8fb0eae7..aa569bbfc8 100644 --- a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html +++ b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html @@ -1,20 +1,13 @@ - - - {{'submission.workflow.tasks.claimed.edit' | translate}} - - - - - - - + +
+ + + + +
+
diff --git a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.spec.ts b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.spec.ts index 36c807bce0..3c134831d9 100644 --- a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.spec.ts +++ b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.spec.ts @@ -1,7 +1,6 @@ import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; -import { By } from '@angular/platform-browser'; import { of as observableOf } from 'rxjs'; import { cold } from 'jasmine-marbles'; @@ -16,11 +15,14 @@ import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.se import { ClaimedTaskActionsComponent } from './claimed-task-actions.component'; import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model'; import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; -import { createSuccessfulRemoteDataObject } from '../../remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../testing/utils'; import { getMockSearchService } from '../../mocks/search-service.mock'; import { getMockRequestService } from '../../mocks/request.service.mock'; import { RequestService } from '../../../core/data/request.service'; import { SearchService } from '../../../core/shared/search/search.service'; +import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; +import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; +import { VarDirective } from '../../utils/var.directive'; let component: ClaimedTaskActionsComponent; let fixture: ComponentFixture; @@ -30,15 +32,15 @@ let notificationsServiceStub: NotificationsServiceStub; let router: RouterStub; let mockDataService; - let searchService; - let requestServce; +let workflowActionService: WorkflowActionDataService; let item; let rdItem; let workflowitem; let rdWorkflowitem; +let workflowAction; function init() { mockDataService = jasmine.createSpyObj('ClaimedTaskDataService', { @@ -46,9 +48,7 @@ function init() { rejectTask: jasmine.createSpy('rejectTask'), returnToPoolTask: jasmine.createSpy('returnToPoolTask'), }); - searchService = getMockSearchService(); - requestServce = getMockRequestService(); item = Object.assign(new Item(), { @@ -84,7 +84,11 @@ function init() { workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) }); rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem); mockObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem), id: '1234' }); + workflowAction = Object.assign(new WorkflowAction(), { id: 'action-1', options: ['option-1', 'option-2'] }); + workflowActionService = jasmine.createSpyObj('workflowActionService', { + findById: createSuccessfulRemoteDataObject$(workflowAction) + }); } describe('ClaimedTaskActionsComponent', () => { @@ -99,14 +103,15 @@ describe('ClaimedTaskActionsComponent', () => { } }) ], - declarations: [ClaimedTaskActionsComponent], + declarations: [ClaimedTaskActionsComponent, VarDirective], providers: [ { provide: Injector, useValue: {} }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: Router, useValue: new RouterStub() }, { provide: ClaimedTaskDataService, useValue: mockDataService }, { provide: SearchService, useValue: searchService }, - { provide: RequestService, useValue: requestServce } + { provide: RequestService, useValue: requestServce }, + { provide: WorkflowActionDataService, useValue: workflowActionService } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ClaimedTaskActionsComponent, { @@ -123,11 +128,6 @@ describe('ClaimedTaskActionsComponent', () => { fixture.detectChanges(); }); - afterEach(() => { - fixture = null; - component = null; - }); - it('should init objects properly', () => { component.object = null; component.initObjects(mockObject); @@ -136,46 +136,14 @@ describe('ClaimedTaskActionsComponent', () => { expect(component.workflowitem$).toBeObservable(cold('(b|)', { b: rdWorkflowitem.payload - })) + })); }); - it('should display edit task button', () => { - const btn = fixture.debugElement.query(By.css('.btn-info')); - - expect(btn).toBeDefined(); - }); - - it('should call approveTask method when approving a task', fakeAsync(() => { - spyOn(component, 'reload'); - mockDataService.approveTask.and.returnValue(observableOf({hasSucceeded: true})); - - component.approve(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(mockDataService.approveTask).toHaveBeenCalledWith(mockObject.id); - }); - - })); - - it('should display a success notification on approve success', async(() => { - spyOn(component, 'reload'); - mockDataService.approveTask.and.returnValue(observableOf({hasSucceeded: true})); - - component.approve(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(notificationsServiceStub.success).toHaveBeenCalled(); - }); - })); - - it('should reload page on approve success', async(() => { + it('should reload page on process completed', async(() => { spyOn(router, 'navigateByUrl'); router.url = 'test.url/test'; - mockDataService.approveTask.and.returnValue(observableOf({hasSucceeded: true})); - component.approve(); + component.handleActionResponse(true); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -183,108 +151,8 @@ describe('ClaimedTaskActionsComponent', () => { }); })); - it('should display an error notification on approve failure', async(() => { - mockDataService.approveTask.and.returnValue(observableOf({hasSucceeded: false})); - - component.approve(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(notificationsServiceStub.error).toHaveBeenCalled(); - }); - })); - - it('should call rejectTask method when rejecting a task', fakeAsync(() => { - spyOn(component, 'reload'); - mockDataService.rejectTask.and.returnValue(observableOf({hasSucceeded: true})); - - component.reject('test reject'); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(mockDataService.rejectTask).toHaveBeenCalledWith('test reject', mockObject.id); - }); - - })); - - it('should display a success notification on reject success', async(() => { - spyOn(component, 'reload'); - mockDataService.rejectTask.and.returnValue(observableOf({hasSucceeded: true})); - - component.reject('test reject'); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(notificationsServiceStub.success).toHaveBeenCalled(); - }); - })); - - it('should reload page on reject success', async(() => { - spyOn(router, 'navigateByUrl'); - router.url = 'test.url/test'; - mockDataService.rejectTask.and.returnValue(observableOf({hasSucceeded: true})); - - component.reject('test reject'); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(router.navigateByUrl).toHaveBeenCalledWith('test.url/test'); - }); - })); - - it('should display an error notification on reject failure', async(() => { - mockDataService.rejectTask.and.returnValue(observableOf({hasSucceeded: false})); - - component.reject('test reject'); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(notificationsServiceStub.error).toHaveBeenCalled(); - }); - })); - - it('should call returnToPoolTask method when returning a task to pool', fakeAsync(() => { - spyOn(component, 'reload'); - mockDataService.returnToPoolTask.and.returnValue(observableOf({hasSucceeded: true})); - - component.returnToPool(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(mockDataService.returnToPoolTask).toHaveBeenCalledWith( mockObject.id); - }); - - })); - - it('should display a success notification on return to pool success', async(() => { - spyOn(component, 'reload'); - mockDataService.returnToPoolTask.and.returnValue(observableOf({hasSucceeded: true})); - - component.returnToPool(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(notificationsServiceStub.success).toHaveBeenCalled(); - }); - })); - - it('should reload page on return to pool success', async(() => { - spyOn(router, 'navigateByUrl'); - router.url = 'test.url/test'; - mockDataService.returnToPoolTask.and.returnValue(observableOf({hasSucceeded: true})); - - component.returnToPool(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(router.navigateByUrl).toHaveBeenCalledWith('test.url/test'); - }); - })); - - it('should display an error notification on return to pool failure', async(() => { - mockDataService.returnToPoolTask.and.returnValue(observableOf({hasSucceeded: false})); - - component.returnToPool(); + it('should display an error notification on process failure', async(() => { + component.handleActionResponse(false); fixture.detectChanges(); fixture.whenStable().then(() => { diff --git a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.ts b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.ts index 81d24fa1d7..c82154af09 100644 --- a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.ts +++ b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.ts @@ -1,13 +1,12 @@ import { Component, Injector, Input, OnInit } from '@angular/core'; import { Router } from '@angular/router'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { Observable } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model'; -import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response'; import { isNotUndefined } from '../../empty.util'; import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; import { RemoteData } from '../../../core/data/remote-data'; @@ -15,6 +14,9 @@ import { MyDSpaceActionsComponent } from '../mydspace-actions'; import { NotificationsService } from '../../notifications/notifications.service'; import { RequestService } from '../../../core/data/request.service'; import { SearchService } from '../../../core/shared/search/search.service'; +import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; +import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; +import { WORKFLOW_TASK_OPTION_RETURN_TO_POOL } from './return-to-pool/claimed-task-actions-return-to-pool.component'; /** * This component represents actions related to ClaimedTask object. @@ -37,19 +39,15 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent; /** - * A boolean representing if an approve operation is pending + * The workflow action available for this task */ - public processingApprove$ = new BehaviorSubject(false); + public actionRD$: Observable>; /** - * A boolean representing if a reject operation is pending + * The option used to render the "return to pool" component + * Every claimed task contains this option */ - public processingReject$ = new BehaviorSubject(false); - - /** - * A boolean representing if a return to pool operation is pending - */ - public processingReturnToPool$ = new BehaviorSubject(false); + public returnToPoolOption = WORKFLOW_TASK_OPTION_RETURN_TO_POOL; /** * Initialize instance variables @@ -60,13 +58,15 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent { - this.processingApprove$.next(false); - this.handleActionResponse(res.hasSucceeded); - }); - } - - /** - * Reject the task. - */ - reject(reason) { - this.processingReject$.next(true); - this.objectDataService.rejectTask(reason, this.object.id) - .subscribe((res: ProcessTaskResponse) => { - this.processingReject$.next(false); - this.handleActionResponse(res.hasSucceeded); - }); - } - - /** - * Return task to the pool. - */ - returnToPool() { - this.processingReturnToPool$.next(true); - this.objectDataService.returnToPoolTask(this.object.id) - .subscribe((res: ProcessTaskResponse) => { - this.processingReturnToPool$.next(false); - this.handleActionResponse(res.hasSucceeded); - }); + initAction(object: ClaimedTask) { + this.actionRD$ = object.action; } } diff --git a/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.html b/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.html new file mode 100644 index 0000000000..4a42378f7e --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.html @@ -0,0 +1,7 @@ + + {{'submission.workflow.tasks.claimed.edit' | translate}} + diff --git a/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.scss b/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.spec.ts b/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.spec.ts new file mode 100644 index 0000000000..912671bd4b --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.spec.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { ClaimedTaskActionsEditMetadataComponent } from './claimed-task-actions-edit-metadata.component'; +import { MockTranslateLoader } from '../../../mocks/mock-translate-loader'; +import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; + +let component: ClaimedTaskActionsEditMetadataComponent; +let fixture: ComponentFixture; + +describe('ClaimedTaskActionsEditMetadataComponent', () => { + const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }) + ], + providers: [ + { provide: ClaimedTaskDataService, useValue: {} } + ], + declarations: [ClaimedTaskActionsEditMetadataComponent], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ClaimedTaskActionsEditMetadataComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ClaimedTaskActionsEditMetadataComponent); + component = fixture.componentInstance; + component.object = object; + fixture.detectChanges(); + }); + + it('should display edit button', () => { + const btn = fixture.debugElement.query(By.css('.btn-primary')); + + expect(btn).toBeDefined(); + }); + +}); diff --git a/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.ts b/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.ts new file mode 100644 index 0000000000..c0ce9cd4e5 --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component'; +import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; + +export const WORKFLOW_TASK_OPTION_EDIT_METADATA = 'submit_edit_metadata'; + +@rendersWorkflowTaskOption(WORKFLOW_TASK_OPTION_EDIT_METADATA) +@Component({ + selector: 'ds-claimed-task-actions-edit-metadata', + styleUrls: ['./claimed-task-actions-edit-metadata.component.scss'], + templateUrl: './claimed-task-actions-edit-metadata.component.html', +}) +/** + * Component for displaying the edit metadata action on a workflow task item + */ +export class ClaimedTaskActionsEditMetadataComponent extends ClaimedTaskActionsAbstractComponent { + /** + * This component represents the edit metadata option + */ + option = WORKFLOW_TASK_OPTION_EDIT_METADATA; + + constructor(protected claimedTaskService: ClaimedTaskDataService) { + super(claimedTaskService); + } +} diff --git a/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.html b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.html index 5ab45f32c7..7c7b83cd8a 100644 --- a/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.html +++ b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.html @@ -1,10 +1,10 @@

- @@ -21,17 +21,17 @@ -
+
diff --git a/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.spec.ts b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.spec.ts index edf9b2bd73..ca78bcc0cc 100644 --- a/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.spec.ts +++ b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.spec.ts @@ -8,6 +8,10 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { ClaimedTaskActionsRejectComponent } from './claimed-task-actions-reject.component'; import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock'; +import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; let component: ClaimedTaskActionsRejectComponent; let fixture: ComponentFixture; @@ -15,6 +19,11 @@ let formBuilder: FormBuilder; let modalService: NgbModal; describe('ClaimedTaskActionsRejectComponent', () => { + const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' }); + const claimedTaskService = jasmine.createSpyObj('claimedTaskService', { + submitTask: observableOf(new ProcessTaskResponse(true)) + }); + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -29,6 +38,7 @@ describe('ClaimedTaskActionsRejectComponent', () => { ], declarations: [ClaimedTaskActionsRejectComponent], providers: [ + { provide: ClaimedTaskDataService, useValue: claimedTaskService }, FormBuilder, NgbModal ], @@ -43,17 +53,11 @@ describe('ClaimedTaskActionsRejectComponent', () => { component = fixture.componentInstance; formBuilder = TestBed.get(FormBuilder); modalService = TestBed.get(NgbModal); + component.object = object; component.modalRef = modalService.open('ok'); fixture.detectChanges(); }); - afterEach(() => { - fixture = null; - component = null; - modalService = null; - formBuilder = null; - }); - it('should init reject form properly', () => { expect(component.rejectForm).toBeDefined(); expect(component.rejectForm instanceof FormGroup).toBeTruthy(); @@ -67,7 +71,7 @@ describe('ClaimedTaskActionsRejectComponent', () => { }); it('should display spin icon when reject is pending', () => { - component.processingReject = true; + component.processing$.next(true); fixture.detectChanges(); const span = fixture.debugElement.query(By.css('.btn-danger .fa-spin')); @@ -87,22 +91,34 @@ describe('ClaimedTaskActionsRejectComponent', () => { component.modalRef.close() }); - it('should call confirmReject on form submit', () => { - spyOn(component.reject, 'emit'); + describe('on form submit', () => { + let expectedBody; - const btn = fixture.debugElement.query(By.css('.btn-danger')); - btn.nativeElement.click(); - fixture.detectChanges(); + beforeEach(() => { + spyOn(component.processCompleted, 'emit'); - expect(component.modalRef).toBeDefined(); + expectedBody = { + [component.option]: 'true', + reason: null + }; - const form = ((document as any).querySelector('form')); - form.dispatchEvent(new Event('ngSubmit')); - fixture.detectChanges(); + const btn = fixture.debugElement.query(By.css('.btn-danger')); + btn.nativeElement.click(); + fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(component.reject.emit).toHaveBeenCalled(); + expect(component.modalRef).toBeDefined(); + + const form = ((document as any).querySelector('form')); + form.dispatchEvent(new Event('ngSubmit')); + fixture.detectChanges(); }); + it('should call claimedTaskService\'s submitTask with the expected body', () => { + expect(claimedTaskService.submitTask).toHaveBeenCalledWith(object.id, expectedBody) + }); + + it('should emit a successful processCompleted event', () => { + expect(component.processCompleted.emit).toHaveBeenCalledWith(true); + }); }); }); diff --git a/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.ts b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.ts index b66c104695..46d40cbb64 100644 --- a/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.ts +++ b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.ts @@ -1,31 +1,27 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; +import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator'; +export const WORKFLOW_TASK_OPTION_REJECT = 'submit_reject'; + +@rendersWorkflowTaskOption(WORKFLOW_TASK_OPTION_REJECT) @Component({ selector: 'ds-claimed-task-actions-reject', styleUrls: ['./claimed-task-actions-reject.component.scss'], templateUrl: './claimed-task-actions-reject.component.html', }) - -export class ClaimedTaskActionsRejectComponent implements OnInit { - +/** + * Component for displaying and processing the reject action on a workflow task item + */ +export class ClaimedTaskActionsRejectComponent extends ClaimedTaskActionsAbstractComponent implements OnInit { /** - * A boolean representing if a reject operation is pending + * This component represents the reject option */ - @Input() processingReject: boolean; - - /** - * CSS classes to append to reject button - */ - @Input() wrapperClass: string; - - /** - * An event fired when a reject action is confirmed. - * Event's payload equals to reject reason. - */ - @Output() reject: EventEmitter = new EventEmitter(); + option = WORKFLOW_TASK_OPTION_REJECT; /** * The reject form group @@ -42,8 +38,12 @@ export class ClaimedTaskActionsRejectComponent implements OnInit { * * @param {FormBuilder} formBuilder * @param {NgbModal} modalService + * @param claimedTaskService */ - constructor(private formBuilder: FormBuilder, private modalService: NgbModal) { + constructor(protected claimedTaskService: ClaimedTaskDataService, + private formBuilder: FormBuilder, + private modalService: NgbModal) { + super(claimedTaskService); } /** @@ -53,17 +53,23 @@ export class ClaimedTaskActionsRejectComponent implements OnInit { this.rejectForm = this.formBuilder.group({ reason: ['', Validators.required] }); - } /** - * Close modal and emit reject event + * Create the request body for rejecting a workflow task + * Includes the reason from the form */ - confirmReject() { - this.processingReject = true; - this.modalRef.close('Send Button'); + createbody(): any { const reason = this.rejectForm.get('reason').value; - this.reject.emit(reason); + return Object.assign(super.createbody(), { reason }); + } + + /** + * Submit a reject option for the task + */ + submitTask() { + this.modalRef.close('Send Button'); + super.submitTask(); } /** diff --git a/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.html b/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.html index 702ce75e7f..66f8e2a058 100644 --- a/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.html +++ b/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.html @@ -1,8 +1,8 @@ diff --git a/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.spec.ts b/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.spec.ts index 0a900be2d6..b84051e53c 100644 --- a/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.spec.ts +++ b/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.spec.ts @@ -4,12 +4,21 @@ import { By } from '@angular/platform-browser'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { ClaimedTaskActionsReturnToPoolComponent } from './claimed-task-actions-return-to-pool.component'; +import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock'; let component: ClaimedTaskActionsReturnToPoolComponent; let fixture: ComponentFixture; describe('ClaimedTaskActionsReturnToPoolComponent', () => { + const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' }); + const claimedTaskService = jasmine.createSpyObj('claimedTaskService', { + returnToPoolTask: observableOf(new ProcessTaskResponse(true)) + }); + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -20,6 +29,9 @@ describe('ClaimedTaskActionsReturnToPoolComponent', () => { } }) ], + providers: [ + { provide: ClaimedTaskDataService, useValue: claimedTaskService } + ], declarations: [ClaimedTaskActionsReturnToPoolComponent], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ClaimedTaskActionsReturnToPoolComponent, { @@ -30,14 +42,10 @@ describe('ClaimedTaskActionsReturnToPoolComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ClaimedTaskActionsReturnToPoolComponent); component = fixture.componentInstance; + component.object = object; fixture.detectChanges(); }); - afterEach(() => { - fixture = null; - component = null; - }); - it('should display return to pool button', () => { const btn = fixture.debugElement.query(By.css('.btn-secondary')); @@ -45,7 +53,7 @@ describe('ClaimedTaskActionsReturnToPoolComponent', () => { }); it('should display spin icon when return to pool action is pending', () => { - component.processingReturnToPool = true; + component.processing$.next(true); fixture.detectChanges(); const span = fixture.debugElement.query(By.css('.btn-secondary .fa-spin')); @@ -53,13 +61,21 @@ describe('ClaimedTaskActionsReturnToPoolComponent', () => { expect(span).toBeDefined(); }); - it('should emit return to pool event', () => { - spyOn(component.returnToPool, 'emit'); + describe('submitTask', () => { + beforeEach(() => { + spyOn(component.processCompleted, 'emit'); - component.confirmReturnToPool(); - fixture.detectChanges(); + component.submitTask(); + fixture.detectChanges(); + }); - expect(component.returnToPool.emit).toHaveBeenCalled(); + it('should call claimedTaskService\'s returnToPoolTask', () => { + expect(claimedTaskService.returnToPoolTask).toHaveBeenCalledWith(object.id) + }); + + it('should emit a successful processCompleted event', () => { + expect(component.processCompleted.emit).toHaveBeenCalledWith(true); + }); }); }); diff --git a/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.ts b/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.ts index 1dfe91eb5b..c53bf30fad 100644 --- a/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.ts +++ b/src/app/shared/mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component.ts @@ -1,32 +1,39 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component } from '@angular/core'; +import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component'; +import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; +import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response'; +export const WORKFLOW_TASK_OPTION_RETURN_TO_POOL = 'return_to_pool'; + +@rendersWorkflowTaskOption(WORKFLOW_TASK_OPTION_RETURN_TO_POOL) @Component({ selector: 'ds-claimed-task-actions-return-to-pool', styleUrls: ['./claimed-task-actions-return-to-pool.component.scss'], templateUrl: './claimed-task-actions-return-to-pool.component.html', }) +/** + * Component for displaying and processing the return to pool action on a workflow task item + */ +export class ClaimedTaskActionsReturnToPoolComponent extends ClaimedTaskActionsAbstractComponent { + /** + * This component represents the return to pool option + */ + option = WORKFLOW_TASK_OPTION_RETURN_TO_POOL; -export class ClaimedTaskActionsReturnToPoolComponent { + constructor(protected claimedTaskService: ClaimedTaskDataService) { + super(claimedTaskService); + } /** - * A boolean representing if a return to pool operation is pending + * Submit a return to pool option for the task */ - @Input() processingReturnToPool: boolean; - - /** - * CSS classes to append to return to pool button - */ - @Input() wrapperClass: string; - - /** - * An event fired when a return to pool action is confirmed. - */ - @Output() returnToPool: EventEmitter = new EventEmitter(); - - /** - * Emit returnToPool event - */ - confirmReturnToPool() { - this.returnToPool.emit(); + submitTask() { + this.processing$.next(true); + this.claimedTaskService.returnToPoolTask(this.object.id) + .subscribe((res: ProcessTaskResponse) => { + this.processing$.next(false); + this.processCompleted.emit(res.hasSucceeded); + }); } } diff --git a/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator.spec.ts b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator.spec.ts new file mode 100644 index 0000000000..04c3183a74 --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator.spec.ts @@ -0,0 +1,39 @@ +import { getComponentByWorkflowTaskOption, rendersWorkflowTaskOption } from './claimed-task-actions-decorator'; + +describe('ClaimedTaskActions decorator function', () => { + const option1 = 'test_option_1'; + const option2 = 'test_option_2'; + const option3 = 'test_option_3'; + + /* tslint:disable:max-classes-per-file */ + class Test1Action {}; + class Test2Action {}; + class Test3Action {}; + /* tslint:enable:max-classes-per-file */ + + beforeAll(() => { + rendersWorkflowTaskOption(option1)(Test1Action); + rendersWorkflowTaskOption(option2)(Test2Action); + rendersWorkflowTaskOption(option3)(Test3Action); + }); + + describe('If there\'s an exact match', () => { + it('should return the matching class', () => { + const component = getComponentByWorkflowTaskOption(option1); + expect(component).toEqual(Test1Action); + + const component2 = getComponentByWorkflowTaskOption(option2); + expect(component2).toEqual(Test2Action); + + const component3 = getComponentByWorkflowTaskOption(option3); + expect(component3).toEqual(Test3Action); + }); + }); + + describe('If there\'s no match', () => { + it('should return unidentified', () => { + const component = getComponentByWorkflowTaskOption('non-existing-option'); + expect(component).toBeUndefined(); + }); + }); +}); diff --git a/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator.ts b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator.ts new file mode 100644 index 0000000000..a115c4e5b8 --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator.ts @@ -0,0 +1,23 @@ +import { hasNoValue } from '../../../empty.util'; + +const map = new Map(); + +/** + * Decorator used for rendering ClaimedTaskActions pages by option type + */ +export function rendersWorkflowTaskOption(option: string) { + return function decorator(component: any) { + if (hasNoValue(map.get(option))) { + map.set(option, component); + } else { + throw new Error(`There can't be more than one component to render ClaimedTaskActions for option "${option}"`); + } + }; +} + +/** + * Get the component used for rendering a ClaimedTaskActions page by option type + */ +export function getComponentByWorkflowTaskOption(option: string) { + return map.get(option); +} diff --git a/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component.html b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component.html new file mode 100644 index 0000000000..364443c47f --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component.html @@ -0,0 +1 @@ + diff --git a/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component.spec.ts b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component.spec.ts new file mode 100644 index 0000000000..b71adc7a25 --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component.spec.ts @@ -0,0 +1,51 @@ +import { ClaimedTaskActionsLoaderComponent } from './claimed-task-actions-loader.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionStrategy, Component, ComponentFactoryResolver, NO_ERRORS_SCHEMA } from '@angular/core'; +import { spyOnExported } from '../../../testing/utils'; +import * as decorators from './claimed-task-actions-decorator'; +import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive'; +import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { ClaimedTaskActionsEditMetadataComponent } from '../edit-metadata/claimed-task-actions-edit-metadata.component'; +import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service'; + +describe('ClaimedTaskActionsLoaderComponent', () => { + let comp: ClaimedTaskActionsLoaderComponent; + let fixture: ComponentFixture; + + const option = 'test_option'; + const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ClaimedTaskActionsLoaderComponent, ClaimedTaskActionsEditMetadataComponent, ClaimedTaskActionsDirective], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: ClaimedTaskDataService, useValue: {} }, + ComponentFactoryResolver + ] + }).overrideComponent(ClaimedTaskActionsLoaderComponent, { + set: { + changeDetection: ChangeDetectionStrategy.Default, + entryComponents: [ClaimedTaskActionsEditMetadataComponent] + } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(ClaimedTaskActionsLoaderComponent); + comp = fixture.componentInstance; + + comp.object = object; + comp.option = option; + spyOnExported(decorators, 'getComponentByWorkflowTaskOption').and.returnValue(ClaimedTaskActionsEditMetadataComponent); + fixture.detectChanges(); + })); + + describe('When the component is rendered', () => { + it('should call the getComponentByWorkflowTaskOption function with the right option', () => { + expect(decorators.getComponentByWorkflowTaskOption).toHaveBeenCalledWith(option); + }) + }); +}); diff --git a/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component.ts b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component.ts new file mode 100644 index 0000000000..d8c8ecccec --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component.ts @@ -0,0 +1,85 @@ +import { + Component, + ComponentFactoryResolver, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewChild +} from '@angular/core'; +import { getComponentByWorkflowTaskOption } from './claimed-task-actions-decorator'; +import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; +import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive'; +import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component'; +import { hasValue } from '../../../empty.util'; +import { Subscription } from 'rxjs/internal/Subscription'; + +@Component({ + selector: 'ds-claimed-task-actions-loader', + templateUrl: './claimed-task-actions-loader.component.html' +}) +/** + * Component for loading a ClaimedTaskAction component depending on the "option" input + * Passes on the ClaimedTask to the component and subscribes to the processCompleted output + */ +export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy { + /** + * The ClaimedTask object + */ + @Input() object: ClaimedTask; + + /** + * The name of the option to render + * Passed on to the decorator to fetch the relevant component for this option + */ + @Input() option: string; + + /** + * Emits the success or failure of a processed action + */ + @Output() processCompleted: EventEmitter = new EventEmitter(); + + /** + * Directive to determine where the dynamic child component is located + */ + @ViewChild(ClaimedTaskActionsDirective, {static: true}) claimedTaskActionsDirective: ClaimedTaskActionsDirective; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + protected subs: Subscription[] = []; + + constructor(private componentFactoryResolver: ComponentFactoryResolver) { + } + + /** + * Fetch, create and initialize the relevant component + */ + ngOnInit(): void { + const comp = getComponentByWorkflowTaskOption(this.option); + if (hasValue(comp)) { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp); + + const viewContainerRef = this.claimedTaskActionsDirective.viewContainerRef; + viewContainerRef.clear(); + + const componentRef = viewContainerRef.createComponent(componentFactory); + const componentInstance = (componentRef.instance as ClaimedTaskActionsAbstractComponent); + componentInstance.object = this.object; + if (hasValue(componentInstance.processCompleted)) { + this.subs.push(componentInstance.processCompleted.subscribe((success) => this.processCompleted.emit(success))); + } + } + } + + /** + * Unsubscribe from open subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } +} diff --git a/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions.directive.ts b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions.directive.ts new file mode 100644 index 0000000000..a4a55b541b --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/switcher/claimed-task-actions.directive.ts @@ -0,0 +1,11 @@ +import { Directive, ViewContainerRef } from '@angular/core'; + +@Directive({ + selector: '[dsClaimedTaskActions]', +}) +/** + * Directive used as a hook to know where to inject the dynamic Claimed Task Actions component + */ +export class ClaimedTaskActionsDirective { + constructor(public viewContainerRef: ViewContainerRef) { } +} diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.spec.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.spec.ts index 8e0f02ab92..a2010691b6 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.spec.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.spec.ts @@ -84,7 +84,12 @@ describe('ClaimedTaskSearchResultDetailElementComponent', () => { it('should init workflowitem properly', (done) => { component.workflowitemRD$.subscribe((workflowitemRD) => { - expect(linkService.resolveLink).toHaveBeenCalled(); + // Make sure the necessary links are being resolved + expect(linkService.resolveLinks).toHaveBeenCalledWith( + component.dso, + jasmine.objectContaining({ name: 'workflowitem' }), + jasmine.objectContaining({ name: 'action' }) + ); expect(workflowitemRD.payload).toEqual(workflowitem); done(); }); diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.ts index 4647a4d4a7..359d3abcdc 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.ts @@ -49,10 +49,10 @@ export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultD */ ngOnInit() { super.ngOnInit(); - this.linkService.resolveLink(this.dso, followLink('workflowitem', null, true, + this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true, followLink('item', null, true, followLink('bundles')), followLink('submitter') - )); + ), followLink('action')); this.workflowitemRD$ = this.dso.workflowitem as Observable>; } diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.spec.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.spec.ts index 13a095dacd..0c2b3a0a70 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.spec.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.spec.ts @@ -86,7 +86,11 @@ describe('PoolSearchResultDetailElementComponent', () => { it('should init workflowitem properly', (done) => { component.workflowitemRD$.subscribe((workflowitemRD) => { - expect(linkService.resolveLink).toHaveBeenCalled(); + expect(linkService.resolveLinks).toHaveBeenCalledWith( + component.dso, + jasmine.objectContaining({ name: 'workflowitem' }), + jasmine.objectContaining({ name: 'action' }) + ); expect(workflowitemRD.payload).toEqual(workflowitem); done(); }); diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.ts index 423931225e..3094d8a98d 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.ts @@ -48,10 +48,10 @@ export class PoolSearchResultDetailElementComponent extends SearchResultDetailEl */ ngOnInit() { super.ngOnInit(); - this.linkService.resolveLink(this.dso, followLink('workflowitem', null, true, + this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true, followLink('item', null, true, followLink('bundles')), followLink('submitter') - )); + ), followLink('action')); this.workflowitemRD$ = this.dso.workflowitem as Observable>; } diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts index de19f5b74a..d065f9c7e4 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts @@ -18,6 +18,7 @@ import { CollectionSearchResult } from '../../../object-collection/shared/collec import { TruncatableService } from '../../../truncatable/truncatable.service'; import { TruncatePipe } from '../../../utils/truncate.pipe'; import { CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component'; +import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; let collectionSearchResultGridElementComponent: CollectionSearchResultGridElementComponent; let fixture: ComponentFixture; @@ -70,6 +71,7 @@ describe('CollectionSearchResultGridElementComponent', () => { { provide: HttpClient, useValue: {} }, { provide: DSOChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: BitstreamFormatDataService, useValue: {} }, ], schemas: [ NO_ERRORS_SCHEMA ] diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts index b97c574970..0d59273111 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts @@ -18,6 +18,7 @@ import { CommunitySearchResult } from '../../../object-collection/shared/communi import { TruncatableService } from '../../../truncatable/truncatable.service'; import { TruncatePipe } from '../../../utils/truncate.pipe'; import { CommunitySearchResultGridElementComponent } from './community-search-result-grid-element.component'; +import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service'; let communitySearchResultGridElementComponent: CommunitySearchResultGridElementComponent; let fixture: ComponentFixture; @@ -70,6 +71,7 @@ describe('CommunitySearchResultGridElementComponent', () => { { provide: HttpClient, useValue: {} }, { provide: DSOChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: BitstreamFormatDataService, useValue: {} }, ], schemas: [ NO_ERRORS_SCHEMA ] diff --git a/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.html b/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.html new file mode 100644 index 0000000000..dfe08144a8 --- /dev/null +++ b/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.html @@ -0,0 +1 @@ +
{{object.name}}
diff --git a/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.ts b/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.ts new file mode 100644 index 0000000000..55eb5b116e --- /dev/null +++ b/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.ts @@ -0,0 +1,16 @@ +import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { Component } from '@angular/core'; +import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../core/shared/view-mode.model'; + +@Component({ + selector: 'ds-bundle-list-element', + templateUrl: './bundle-list-element.component.html' +}) +/** + * This component is automatically used to create a list view for Bundle objects + */ +@listableObjectComponent(Bundle, ViewMode.ListElement) +export class BundleListElementComponent extends AbstractListableElementComponent { +} diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts index 1ba2877334..f06f3eac81 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts @@ -86,7 +86,11 @@ describe('ClaimedSearchResultListElementComponent', () => { it('should init workflowitem properly', (done) => { component.workflowitemRD$.subscribe((workflowitemRD) => { - expect(linkService.resolveLink).toHaveBeenCalled(); + expect(linkService.resolveLinks).toHaveBeenCalledWith( + component.dso, + jasmine.objectContaining({ name: 'workflowitem' }), + jasmine.objectContaining({ name: 'action' }) + ); expect(workflowitemRD.payload).toEqual(workflowitem); done(); }); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts index cb46e25282..d149595514 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts @@ -55,9 +55,9 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle */ ngOnInit() { super.ngOnInit(); - this.linkService.resolveLink(this.dso, followLink('workflowitem', null, true, + this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true, followLink('item'), followLink('submitter') - )); + ), followLink('action')); this.workflowitemRD$ = this.dso.workflowitem as Observable>; } } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts index 21d9d059ca..fbeb63c667 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts @@ -86,7 +86,11 @@ describe('PoolSearchResultListElementComponent', () => { it('should init workflowitem properly', (done) => { component.workflowitemRD$.subscribe((workflowitemRD) => { - expect(linkService.resolveLink).toHaveBeenCalled(); + expect(linkService.resolveLinks).toHaveBeenCalledWith( + component.dso, + jasmine.objectContaining({ name: 'workflowitem' }), + jasmine.objectContaining({ name: 'action' }) + ); expect(workflowitemRD.payload).toEqual(workflowitem); done(); }); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index 8ab00f4b9b..0953af3c76 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -58,9 +58,9 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen */ ngOnInit() { super.ngOnInit(); - this.linkService.resolveLink(this.dso, followLink('workflowitem', null, true, + this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true, followLink('item'), followLink('submitter') - )); + ), followLink('action')); this.workflowitemRD$ = this.dso.workflowitem as Observable>; } } diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts new file mode 100644 index 0000000000..84f3381880 --- /dev/null +++ b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts @@ -0,0 +1,178 @@ +import { AbstractPaginatedDragAndDropListComponent } from './abstract-paginated-drag-and-drop-list.component'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; +import { ElementRef } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { RemoteData } from '../../core/data/remote-data'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { createPaginatedList, createSuccessfulRemoteDataObject } from '../testing/utils'; +import { FieldUpdates } from '../../core/data/object-updates/object-updates.reducer'; +import { of as observableOf } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { PaginationComponent } from '../pagination/pagination.component'; + +class MockAbstractPaginatedDragAndDropListComponent extends AbstractPaginatedDragAndDropListComponent { + + constructor(protected objectUpdatesService: ObjectUpdatesService, + protected elRef: ElementRef, + protected mockUrl: string, + protected mockObjectsRD$: Observable>>) { + super(objectUpdatesService, elRef); + } + + initializeObjectsRD(): void { + this.objectsRD$ = this.mockObjectsRD$; + } + + initializeURL(): void { + this.url = this.mockUrl; + } +} + +describe('AbstractPaginatedDragAndDropListComponent', () => { + let component: MockAbstractPaginatedDragAndDropListComponent; + let objectUpdatesService: ObjectUpdatesService; + let elRef: ElementRef; + + const url = 'mock-abstract-paginated-drag-and-drop-list-component'; + + const object1 = Object.assign(new DSpaceObject(), { uuid: 'object-1' }); + const object2 = Object.assign(new DSpaceObject(), { uuid: 'object-2' }); + const objectsRD = createSuccessfulRemoteDataObject(createPaginatedList([object1, object2])); + let objectsRD$: BehaviorSubject>>; + + const updates = { + [object1.uuid]: { field: object1, changeType: undefined }, + [object2.uuid]: { field: object2, changeType: undefined } + } as FieldUpdates; + + let paginationComponent: PaginationComponent; + + beforeEach(() => { + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { + initializeWithCustomOrder: {}, + addPageToCustomOrder: {}, + getFieldUpdatesByCustomOrder: observableOf(updates), + saveMoveFieldUpdate: {} + }); + elRef = { + nativeElement: jasmine.createSpyObj('nativeElement', { + querySelector: {} + }) + }; + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: {} + }); + objectsRD$ = new BehaviorSubject(objectsRD); + component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, url, objectsRD$); + component.paginationComponent = paginationComponent; + component.ngOnInit(); + }); + + it('should call initializeWithCustomOrder to initialize the first page and add it to initializedPages', (done) => { + expect(component.initializedPages.indexOf(0)).toBeLessThan(0); + component.updates$.pipe(take(1)).subscribe(() => { + expect(objectUpdatesService.initializeWithCustomOrder).toHaveBeenCalled(); + expect(component.initializedPages.indexOf(0)).toBeGreaterThanOrEqual(0); + done(); + }); + }); + + it('should initialize the updates correctly', (done) => { + component.updates$.pipe(take(1)).subscribe((fieldUpdates) => { + expect(fieldUpdates).toEqual(updates); + done(); + }); + }); + + describe('when a new page is loaded', () => { + const page = 5; + + beforeEach((done) => { + component.updates$.pipe(take(1)).subscribe(() => { + component.currentPage$.next(page); + objectsRD$.next(objectsRD); + done(); + }); + }); + + it('should call addPageToCustomOrder to initialize the new page and add it to initializedPages', (done) => { + component.updates$.pipe(take(1)).subscribe(() => { + expect(objectUpdatesService.addPageToCustomOrder).toHaveBeenCalled(); + expect(component.initializedPages.indexOf(page - 1)).toBeGreaterThanOrEqual(0); + done(); + }); + }); + + describe('twice', () => { + beforeEach((done) => { + component.updates$.pipe(take(1)).subscribe(() => { + component.currentPage$.next(page); + objectsRD$.next(objectsRD); + done(); + }); + }); + + it('shouldn\'t call addPageToCustomOrder again, as the page has already been initialized', (done) => { + component.updates$.pipe(take(1)).subscribe(() => { + expect(objectUpdatesService.addPageToCustomOrder).toHaveBeenCalledTimes(1); + done(); + }); + }); + }); + }); + + describe('switchPage', () => { + const page = 3; + + beforeEach(() => { + component.switchPage(page); + }); + + it('should set currentPage$ to the new page', () => { + expect(component.currentPage$.value).toEqual(page); + }); + }); + + describe('drop', () => { + const event = { + previousIndex: 0, + currentIndex: 1, + item: { element: { nativeElement: { id: object1.uuid } } } + } as any; + + describe('when the user is hovering over a new page', () => { + const hoverPage = 3; + const hoverElement = { textContent: '' + hoverPage }; + + beforeEach(() => { + elRef.nativeElement.querySelector.and.returnValue(hoverElement); + component.initializedPages.push(hoverPage - 1); + component.drop(event); + }); + + it('should detect the page and set currentPage$ to its value', () => { + expect(component.currentPage$.value).toEqual(hoverPage); + }); + + it('should detect the page and update the pagination component with its value', () => { + expect(paginationComponent.doPageChange).toHaveBeenCalledWith(hoverPage); + }); + + it('should send out a saveMoveFieldUpdate with the correct values', () => { + expect(objectUpdatesService.saveMoveFieldUpdate).toHaveBeenCalledWith(url, event.previousIndex, 0, 0, hoverPage - 1, object1); + }); + }); + + describe('when the user is not hovering over a new page', () => { + beforeEach(() => { + component.drop(event); + }); + + it('should send out a saveMoveFieldUpdate with the correct values', () => { + expect(objectUpdatesService.saveMoveFieldUpdate).toHaveBeenCalledWith(url, event.previousIndex, event.currentIndex, 0, 0); + }); + }); + }); +}); diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts new file mode 100644 index 0000000000..a34b5d5bc0 --- /dev/null +++ b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts @@ -0,0 +1,195 @@ +import { FieldUpdates } from '../../core/data/object-updates/object-updates.reducer'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; +import { switchMap, take, tap } from 'rxjs/operators'; +import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; +import { paginatedListToArray } from '../../core/shared/operators'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { ElementRef, ViewChild } from '@angular/core'; +import { PaginationComponent } from '../pagination/pagination.component'; + +/** + * An abstract component containing general methods and logic to be able to drag and drop objects within a paginated + * list. This implementation supports being able to drag and drop objects between pages. + * Dragging an object on top of a page number will automatically detect the page it's being dropped on, send an update + * to the store and add the object on top of that page. + * + * To extend this component, it is important to make sure to: + * - Initialize objectsRD$ within the initializeObjectsRD() method + * - Initialize a unique URL for this component/page within the initializeURL() method + * - Add (cdkDropListDropped)="drop($event)" to the cdkDropList element in your template + * - Add (pageChange)="switchPage($event)" to the ds-pagination element in your template + * - Use the updates$ observable for building your list of cdkDrag elements in your template + * + * An example component extending from this abstract component: PaginatedDragAndDropBitstreamListComponent + */ +export abstract class AbstractPaginatedDragAndDropListComponent { + /** + * A view on the child pagination component + */ + @ViewChild(PaginationComponent, {static: false}) paginationComponent: PaginationComponent; + + /** + * The URL to use for accessing the object updates from this list + */ + url: string; + + /** + * The objects to retrieve data for and transform into field updates + */ + objectsRD$: Observable>>; + + /** + * The updates to the current list + */ + updates$: Observable; + + /** + * The amount of objects to display per page + */ + pageSize = 10; + + /** + * The page options to use for fetching the objects + * Start at page 1 and always use the set page size + */ + options = Object.assign(new PaginationComponentOptions(),{ + id: 'paginated-drag-and-drop-options', + currentPage: 1, + pageSize: this.pageSize + }); + + /** + * The current page being displayed + */ + currentPage$ = new BehaviorSubject(1); + + /** + * A list of pages that have been initialized in the field-update store + */ + initializedPages: number[] = []; + + /** + * An object storing information about an update that should be fired whenever fireToUpdate is called + */ + toUpdate: { + fromIndex: number, + toIndex: number, + fromPage: number, + toPage: number, + field?: T + }; + + protected constructor(protected objectUpdatesService: ObjectUpdatesService, + protected elRef: ElementRef) { + } + + /** + * Initialize the observables + */ + ngOnInit() { + this.initializeObjectsRD(); + this.initializeURL(); + this.initializeUpdates(); + } + + /** + * Overwrite this method to define how the list of objects is initialized and updated + */ + abstract initializeObjectsRD(): void; + + /** + * Overwrite this method to define how the URL is set + */ + abstract initializeURL(): void; + + /** + * Initialize the field-updates in the store + * This method ensures (new) pages displayed are automatically added to the field-update store when the objectsRD updates + */ + initializeUpdates(): void { + this.updates$ = this.objectsRD$.pipe( + paginatedListToArray(), + tap((objects: T[]) => { + // Pages in the field-update store are indexed starting at 0 (because they're stored in an array of pages) + const updatesPage = this.currentPage$.value - 1; + if (isEmpty(this.initializedPages)) { + // No updates have been initialized yet for this list, initialize the first page + this.objectUpdatesService.initializeWithCustomOrder(this.url, objects, new Date(), this.pageSize, updatesPage); + this.initializedPages.push(updatesPage); + } else if (this.initializedPages.indexOf(updatesPage) < 0) { + // Updates were initialized for this list, but not the page we're on. Add the current page to the field-update store for this list + this.objectUpdatesService.addPageToCustomOrder(this.url, objects, updatesPage); + this.initializedPages.push(updatesPage); + } + + // The new page is loaded into the store, check if there are any updates waiting and fire those as well + this.fireToUpdate(); + }), + switchMap((objects: T[]) => this.objectUpdatesService.getFieldUpdatesByCustomOrder(this.url, objects, this.currentPage$.value - 1)) + ); + } + + /** + * Update the current page + * @param page + */ + switchPage(page: number) { + this.currentPage$.next(page); + } + + /** + * An object was moved, send updates to the store. + * When the object is dropped on a page within the pagination of this component, the object moves to the top of that + * page and the pagination automatically loads and switches the view to that page. + * @param event + */ + drop(event: CdkDragDrop) { + // Check if the user is hovering over any of the pagination's pages at the time of dropping the object + const droppedOnElement = this.elRef.nativeElement.querySelector('.page-item:hover'); + if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent)) { + // The user is hovering over a page, fetch the page's number from the element + const page = Number(droppedOnElement.textContent); + if (hasValue(page) && !Number.isNaN(page)) { + const id = event.item.element.nativeElement.id; + this.updates$.pipe(take(1)).subscribe((updates: FieldUpdates) => { + const field = hasValue(updates[id]) ? updates[id].field : undefined; + this.toUpdate = Object.assign({ + fromIndex: event.previousIndex, + toIndex: 0, + fromPage: this.currentPage$.value - 1, + toPage: page - 1, + field + }); + // Switch to the dropped-on page and force a page update for the pagination component + this.currentPage$.next(page); + this.paginationComponent.doPageChange(page); + if (this.initializedPages.indexOf(page - 1) >= 0) { + // The page the object is being dropped to has already been loaded before, directly fire an update to the store. + // For pages that haven't been loaded before, the updates$ observable will call fireToUpdate after the new page + // has loaded + this.fireToUpdate(); + } + }); + } + } else { + this.objectUpdatesService.saveMoveFieldUpdate(this.url, event.previousIndex, event.currentIndex, this.currentPage$.value - 1, this.currentPage$.value - 1); + } + } + + /** + * Method checking if there's an update ready to be fired. Send out a MoveFieldUpdate to the store if there's an + * update present and clear the update afterwards. + */ + fireToUpdate() { + if (hasValue(this.toUpdate)) { + this.objectUpdatesService.saveMoveFieldUpdate(this.url, this.toUpdate.fromIndex, this.toUpdate.toIndex, this.toUpdate.fromPage, this.toUpdate.toPage, this.toUpdate.field); + this.toUpdate = undefined; + } + } +} diff --git a/src/app/shared/responsive-table-sizes/responsive-column-sizes.spec.ts b/src/app/shared/responsive-table-sizes/responsive-column-sizes.spec.ts new file mode 100644 index 0000000000..843d0f043e --- /dev/null +++ b/src/app/shared/responsive-table-sizes/responsive-column-sizes.spec.ts @@ -0,0 +1,22 @@ +import { ResponsiveColumnSizes } from './responsive-column-sizes'; + +describe('ResponsiveColumnSizes', () => { + const xs = 2; + const sm = 3; + const md = 4; + const lg = 6; + const xl = 8; + const column = new ResponsiveColumnSizes(xs, sm, md, lg, xl); + + describe('buildClasses', () => { + let classes: string; + + beforeEach(() => { + classes = column.buildClasses(); + }); + + it('should return the correct bootstrap classes', () => { + expect(classes).toEqual(`col-${xs} col-sm-${sm} col-md-${md} col-lg-${lg} col-xl-${xl}`); + }); + }); +}); diff --git a/src/app/shared/responsive-table-sizes/responsive-column-sizes.ts b/src/app/shared/responsive-table-sizes/responsive-column-sizes.ts new file mode 100644 index 0000000000..84651f3ef5 --- /dev/null +++ b/src/app/shared/responsive-table-sizes/responsive-column-sizes.ts @@ -0,0 +1,46 @@ +/** + * A helper class storing the sizes in which to render a single column + * The values in this class are expected to be between 1 and 12 + * There are used to be added to bootstrap classes such as col-xs-{this.xs} + */ +export class ResponsiveColumnSizes { + /** + * The extra small bootstrap size + */ + xs: number; + + /** + * The small bootstrap size + */ + sm: number; + + /** + * The medium bootstrap size + */ + md: number; + + /** + * The large bootstrap size + */ + lg: number; + + /** + * The extra large bootstrap size + */ + xl: number; + + constructor(xs: number, sm: number, md: number, lg: number, xl: number) { + this.xs = xs; + this.sm = sm; + this.md = md; + this.lg = lg; + this.xl = xl; + } + + /** + * Build the bootstrap responsive column classes matching the values of this object + */ + buildClasses(): string { + return `col-${this.xs} col-sm-${this.sm} col-md-${this.md} col-lg-${this.lg} col-xl-${this.xl}` + } +} diff --git a/src/app/shared/responsive-table-sizes/responsive-table-sizes.spec.ts b/src/app/shared/responsive-table-sizes/responsive-table-sizes.spec.ts new file mode 100644 index 0000000000..23df9b1c25 --- /dev/null +++ b/src/app/shared/responsive-table-sizes/responsive-table-sizes.spec.ts @@ -0,0 +1,76 @@ +import { ResponsiveColumnSizes } from './responsive-column-sizes'; +import { ResponsiveTableSizes } from './responsive-table-sizes'; + +describe('ResponsiveColumnSizes', () => { + const column0 = new ResponsiveColumnSizes(2, 3, 4, 6, 8); + const column1 = new ResponsiveColumnSizes(8, 7, 4, 2, 1); + const column2 = new ResponsiveColumnSizes(1, 1, 4, 2, 1); + const column3 = new ResponsiveColumnSizes(1, 1, 4, 2, 2); + const table = new ResponsiveTableSizes([column0, column1, column2, column3]); + + describe('combineColumns', () => { + describe('when start value is out of bounds', () => { + let combined: ResponsiveColumnSizes; + + beforeEach(() => { + combined = table.combineColumns(-1, 2); + }); + + it('should return undefined', () => { + expect(combined).toBeUndefined(); + }); + }); + + describe('when end value is out of bounds', () => { + let combined: ResponsiveColumnSizes; + + beforeEach(() => { + combined = table.combineColumns(0, 5); + }); + + it('should return undefined', () => { + expect(combined).toBeUndefined(); + }); + }); + + describe('when start value is greater than end value', () => { + let combined: ResponsiveColumnSizes; + + beforeEach(() => { + combined = table.combineColumns(2, 0); + }); + + it('should return undefined', () => { + expect(combined).toBeUndefined(); + }); + }); + + describe('when start value is equal to end value', () => { + let combined: ResponsiveColumnSizes; + + beforeEach(() => { + combined = table.combineColumns(0, 0); + }); + + it('should return undefined', () => { + expect(combined).toBeUndefined(); + }); + }); + + describe('when provided with valid values', () => { + let combined: ResponsiveColumnSizes; + + beforeEach(() => { + combined = table.combineColumns(0, 2); + }); + + it('should combine the sizes of each column within the range into one', () => { + expect(combined.xs).toEqual(column0.xs + column1.xs + column2.xs); + expect(combined.sm).toEqual(column0.sm + column1.sm + column2.sm); + expect(combined.md).toEqual(column0.md + column1.md + column2.md); + expect(combined.lg).toEqual(column0.lg + column1.lg + column2.lg); + expect(combined.xl).toEqual(column0.xl + column1.xl + column2.xl); + }); + }); + }); +}); diff --git a/src/app/shared/responsive-table-sizes/responsive-table-sizes.ts b/src/app/shared/responsive-table-sizes/responsive-table-sizes.ts new file mode 100644 index 0000000000..b68774d46f --- /dev/null +++ b/src/app/shared/responsive-table-sizes/responsive-table-sizes.ts @@ -0,0 +1,42 @@ +import { ResponsiveColumnSizes } from './responsive-column-sizes'; +import { hasValue } from '../empty.util'; + +/** + * A helper class storing the sizes in which to render a table + * It stores a list of columns, which in turn store their own bootstrap column sizes + */ +export class ResponsiveTableSizes { + /** + * A list of all the columns and their responsive sizes within this table + */ + columns: ResponsiveColumnSizes[]; + + constructor(columns: ResponsiveColumnSizes[]) { + this.columns = columns; + } + + /** + * Combine the values of multiple columns into a single ResponsiveColumnSizes + * Useful when a row element stretches over multiple columns + * @param start Index of the first column + * @param end Index of the last column (inclusive) + */ + combineColumns(start: number, end: number): ResponsiveColumnSizes { + if (start < end && hasValue(this.columns[start]) && hasValue(this.columns[end])) { + let xs = this.columns[start].xs; + let sm = this.columns[start].sm; + let md = this.columns[start].md; + let lg = this.columns[start].lg; + let xl = this.columns[start].xl; + for (let i = start + 1; i < end + 1; i++) { + xs += this.columns[i].xs; + sm += this.columns[i].sm; + md += this.columns[i].md; + lg += this.columns[i].lg; + xl += this.columns[i].xl; + } + return new ResponsiveColumnSizes(xs, sm, md, lg, xl); + } + return undefined; + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 014353bdad..f66bb63848 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -9,6 +9,7 @@ import { NgbDatepickerModule, NgbModule, NgbTimepickerModule, NgbTypeaheadModule import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core'; import { NgxPaginationModule } from 'ngx-pagination'; +import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component'; import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component'; import { FileUploadModule } from 'ng2-file-upload'; @@ -184,8 +185,13 @@ import { LogInContainerComponent } from './log-in/container/log-in-container.com import { LogInShibbolethComponent } from './log-in/methods/shibboleth/log-in-shibboleth.component'; import { LogInPasswordComponent } from './log-in/methods/password/log-in-password.component'; import { LogInComponent } from './log-in/log-in.component'; +import { CustomSwitchComponent } from './form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component'; +import { BundleListElementComponent } from './object-list/bundle-list-element/bundle-list-element.component'; import { MissingTranslationHelper } from './translate/missing-translation.helper'; import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component'; +import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component'; +import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive'; +import { ClaimedTaskActionsEditMetadataComponent } from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -247,6 +253,7 @@ const COMPONENTS = [ EditComColPageComponent, DeleteComColPageComponent, ComcolPageBrowseByComponent, + ComcolRoleComponent, DsDynamicFormComponent, DsDynamicFormControlContainerComponent, DsDynamicListComponent, @@ -291,6 +298,8 @@ const COMPONENTS = [ ClaimedTaskActionsApproveComponent, ClaimedTaskActionsRejectComponent, ClaimedTaskActionsReturnToPoolComponent, + ClaimedTaskActionsEditMetadataComponent, + ClaimedTaskActionsLoaderComponent, ItemActionsComponent, PoolTaskActionsComponent, WorkflowitemActionsComponent, @@ -345,6 +354,9 @@ const COMPONENTS = [ AbstractTrackableComponent, ComcolMetadataComponent, ItemTypeBadgeComponent, + BrowseByComponent, + AbstractTrackableComponent, + CustomSwitchComponent, ItemSelectComponent, CollectionSelectComponent, MetadataRepresentationLoaderComponent, @@ -404,6 +416,7 @@ const ENTRY_COMPONENTS = [ PlainTextMetadataListElementComponent, ItemMetadataListElementComponent, MetadataRepresentationListElementComponent, + CustomSwitchComponent, ItemMetadataRepresentationListElementComponent, SearchResultsComponent, CollectionSearchResultGridElementComponent, @@ -424,7 +437,12 @@ const ENTRY_COMPONENTS = [ LogInPasswordComponent, LogInShibbolethComponent, ItemVersionsComponent, - ItemVersionsNoticeComponent + BundleListElementComponent, + ItemVersionsNoticeComponent, + ClaimedTaskActionsApproveComponent, + ClaimedTaskActionsRejectComponent, + ClaimedTaskActionsReturnToPoolComponent, + ClaimedTaskActionsEditMetadataComponent ]; const SHARED_ITEM_PAGE_COMPONENTS = [ @@ -452,7 +470,8 @@ const DIRECTIVES = [ AutoFocusDirective, RoleDirective, MetadataRepresentationDirective, - ListableObjectDirective + ListableObjectDirective, + ClaimedTaskActionsDirective ]; @NgModule({ diff --git a/src/app/shared/testing/eperson.mock.ts b/src/app/shared/testing/eperson.mock.ts index 81a5a48a2c..61cf9e8b33 100644 --- a/src/app/shared/testing/eperson.mock.ts +++ b/src/app/shared/testing/eperson.mock.ts @@ -3,7 +3,7 @@ import { GroupMock } from './group-mock'; export const EPersonMock: EPerson = Object.assign(new EPerson(), { handle: null, - groups: [], + groups: [GroupMock], netid: 'test@test.com', lastActive: '2018-05-14T12:25:42.411+0000', canLogIn: true, @@ -49,7 +49,7 @@ export const EPersonMock: EPerson = Object.assign(new EPerson(), { export const EPersonMock2: EPerson = Object.assign(new EPerson(), { handle: null, - groups: [GroupMock], + groups: [], netid: 'test2@test.com', lastActive: '2019-05-14T12:25:42.411+0000', canLogIn: false, diff --git a/src/app/shared/testing/group-mock.ts b/src/app/shared/testing/group-mock.ts index 0c9abb4b7d..00068a5eea 100644 --- a/src/app/shared/testing/group-mock.ts +++ b/src/app/shared/testing/group-mock.ts @@ -1,16 +1,38 @@ import { Group } from '../../core/eperson/models/group.model'; +import { EPersonMock } from './eperson-mock'; + +export const GroupMock2: Group = Object.assign(new Group(), { + handle: null, + subgroups: [], + epersons: [], + permanent: true, + selfRegistered: false, + _links: { + self: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2', + }, + subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/subgroups' }, + epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons' } + }, + id: 'testgroupid2', + uuid: 'testgroupid2', + type: 'group', +}); export const GroupMock: Group = Object.assign(new Group(), { - handle: null, - groups: [], - selfRegistered: false, - _links: { - self: { - href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid', + handle: null, + subgroups: [GroupMock2], + epersons: [EPersonMock], + selfRegistered: false, + permanent: false, + _links: { + self: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid', + }, + subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/subgroups' }, + epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons' } }, - groups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups' } - }, - id: 'testgroupid', - uuid: 'testgroupid', - type: 'group', + id: 'testgroupid', + uuid: 'testgroupid', + type: 'group', }); diff --git a/src/app/shared/trackable/abstract-trackable.component.ts b/src/app/shared/trackable/abstract-trackable.component.ts index bb1f4b31b4..e1a99d90b9 100644 --- a/src/app/shared/trackable/abstract-trackable.component.ts +++ b/src/app/shared/trackable/abstract-trackable.component.ts @@ -63,7 +63,7 @@ export class AbstractTrackableComponent { * Get translated notification title * @param key */ - protected getNotificationTitle(key: string) { + getNotificationTitle(key: string) { return this.translateService.instant(this.notificationsPrefix + key + '.title'); } @@ -71,7 +71,7 @@ export class AbstractTrackableComponent { * Get translated notification content * @param key */ - protected getNotificationContent(key: string) { + getNotificationContent(key: string) { return this.translateService.instant(this.notificationsPrefix + key + '.content'); } diff --git a/src/app/shared/uploader/uploader-properties.model.ts b/src/app/shared/uploader/uploader-properties.model.ts new file mode 100644 index 0000000000..bc0376b809 --- /dev/null +++ b/src/app/shared/uploader/uploader-properties.model.ts @@ -0,0 +1,21 @@ +import { MetadataMap } from '../../core/shared/metadata.models'; + +/** + * Properties to send to the REST API for uploading a bitstream + */ +export class UploaderProperties { + /** + * A custom name for the bitstream + */ + name: string; + + /** + * Metadata for the bitstream (e.g. dc.description) + */ + metadata: MetadataMap; + + /** + * The name of the bundle to upload the bitstream to + */ + bundleName: string; +} diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/uploader/uploader.component.ts index 935d196d08..72a38d1eb1 100644 --- a/src/app/shared/uploader/uploader.component.ts +++ b/src/app/shared/uploader/uploader.component.ts @@ -15,8 +15,9 @@ import { uniqueId } from 'lodash'; import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; import { UploaderOptions } from './uploader-options.model'; -import { isNotEmpty, isUndefined } from '../empty.util'; +import { hasValue, isNotEmpty, isUndefined } from '../empty.util'; import { UploaderService } from './uploader.service'; +import { UploaderProperties } from './uploader-properties.model'; @Component({ selector: 'ds-uploader', @@ -53,6 +54,11 @@ export class UploaderComponent { */ @Input() uploadFilesOptions: UploaderOptions; + /** + * Extra properties to be passed with the form-data of the upload + */ + @Input() uploadProperties: UploaderProperties; + /** * The function to call when upload is completed */ @@ -131,6 +137,11 @@ export class UploaderComponent { }; this.scrollToService.scrollTo(config); }; + if (hasValue(this.uploadProperties)) { + this.uploader.onBuildItemForm = (item, form) => { + form.append('properties', JSON.stringify(this.uploadProperties)) + }; + } this.uploader.onCompleteItem = (item: any, response: any, status: any, headers: any) => { if (isNotEmpty(response)) { const responsePath = JSON.parse(response); diff --git a/src/app/shared/utils/object-values-pipe.ts b/src/app/shared/utils/object-values-pipe.ts index 331f78b452..5fab8bf841 100644 --- a/src/app/shared/utils/object-values-pipe.ts +++ b/src/app/shared/utils/object-values-pipe.ts @@ -1,6 +1,10 @@ import { PipeTransform, Pipe } from '@angular/core'; +import { isNotEmpty } from '../empty.util'; -@Pipe({name: 'dsObjectValues'}) +@Pipe({ + name: 'dsObjectValues', + pure: true +}) /** * Pipe for parsing all values of an object to an array of values */ @@ -12,7 +16,9 @@ export class ObjectValuesPipe implements PipeTransform { */ transform(value): any { const values = []; - Object.values(value).forEach((v) => values.push(v)); + if (isNotEmpty(value)) { + Object.values(value).forEach((v) => values.push(v)); + } return values; } } diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index 0833dd7a04..482965b6b4 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectorRef, Component, Inject } from '@angular/core'; import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription} from 'rxjs'; import { distinctUntilChanged, filter, find, flatMap, map, reduce, take, tap } from 'rxjs/operators'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { SectionModelComponent } from '../models/section.model'; import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shared/empty.util'; @@ -23,7 +22,6 @@ import { Group } from '../../../core/eperson/models/group.model'; import { SectionsService } from '../sections.service'; import { SubmissionService } from '../../submission.service'; import { Collection } from '../../../core/shared/collection.model'; -import { ResourcePolicy } from '../../../core/shared/resource-policy.model'; import { AccessConditionOption } from '../../../core/config/models/config-access-condition-option.model'; import { PaginatedList } from '../../../core/data/paginated-list'; @@ -205,7 +203,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { mapGroups$.push( this.groupService.findById(accessCondition.selectGroupUUID).pipe( find((rd: RemoteData) => !rd.isResponsePending && rd.hasSucceeded), - flatMap((group: RemoteData) => group.payload.groups), + flatMap((group: RemoteData) => group.payload.subgroups), find((rd: RemoteData>) => !rd.isResponsePending && rd.hasSucceeded), map((rd: RemoteData>) => ({ accessCondition: accessCondition.name, diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index ade65d7052..563e6f9232 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -1,1605 +1,2922 @@ { + // "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": "Die Seite, die Sie aufrufen wollten, konnte nicht gefunden werden. Sie könnte verschoben oder gelöscht worden sein. Mit dem Link unten kommen Sie zurück zur Startseite. ", + // "404.link.home-page": "Take me to the home page", "404.link.home-page": "Zurück zur Startseite", + // "404.page-not-found": "page not found", "404.page-not-found": "Seite nicht gefunden", + + // "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", "admin.registries.bitstream-formats.create.failure.content": "Ein Fehler ist beim Anlegen eines neuen Dateiformates aufgetreten.", + // "admin.registries.bitstream-formats.create.failure.head": "Failure", "admin.registries.bitstream-formats.create.failure.head": "Fehler", + // "admin.registries.bitstream-formats.create.head": "Create Bitstream format", "admin.registries.bitstream-formats.create.head": "Neues Dateiformat anlegen", + // "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", "admin.registries.bitstream-formats.create.new": "Ein neues Dateiformat hinzufügen", + // "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", "admin.registries.bitstream-formats.create.success.content": "Das neue Dateiformat wurde erfolgreich angelegt.", + // "admin.registries.bitstream-formats.create.success.head": "Success", "admin.registries.bitstream-formats.create.success.head": "Erfolg", + // "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", - "admin.registries.bitstream-formats.delete.failure.amount": "{{ amount }} Format(1) konnte(n) nicht gelöscht werden", + "admin.registries.bitstream-formats.delete.failure.amount": "{{ amount }} Format(e) konnte(n) nicht gelöscht werden", + // "admin.registries.bitstream-formats.delete.failure.head": "Failure", "admin.registries.bitstream-formats.delete.failure.head": "Fehler", + // "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", "admin.registries.bitstream-formats.delete.success.amount": "{{ amount }} Format(e) erfolgreich gelöscht", + // "admin.registries.bitstream-formats.delete.success.head": "Success", "admin.registries.bitstream-formats.delete.success.head": "Erfolg", + // "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", "admin.registries.bitstream-formats.description": "Die Liste der Dateiformate enthält Informationen über bekannte Formate und deren Unterstützungsgrad.", + // "admin.registries.bitstream-formats.edit.description.hint": "", "admin.registries.bitstream-formats.edit.description.hint": "", + // "admin.registries.bitstream-formats.edit.description.label": "Description", "admin.registries.bitstream-formats.edit.description.label": "Beschreibung", + // "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": "Extensionen sind Dateieindungen, welche zur Identifizierung der Formate von hochgeladenen Dateien dienen. Sie können mehrere Endungen pro Format angeben.", + // "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", "admin.registries.bitstream-formats.edit.extensions.label": "Dateiendungen", + // "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot", "admin.registries.bitstream-formats.edit.extensions.placeholder": "Geben Sie die Endung ohne Punkt ein", + // "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", "admin.registries.bitstream-formats.edit.failure.content": "Ein Fehler ist beim Editieren des Dateiformates", + // "admin.registries.bitstream-formats.edit.failure.head": "Failure", "admin.registries.bitstream-formats.edit.failure.head": "Fehler", + // "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", "admin.registries.bitstream-formats.edit.head": "Dateiformat: {{ 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": "Dateiformate, die als intern gekennzeichnit sind, dienen administrativen Zwecken und bleiben dem Endnutzer verborgen.", + "admin.registries.bitstream-formats.edit.internal.hint": "Dateiformate, die als intern gekennzeichnet sind, dienen administrativen Zwecken und bleiben dem Endnutzer verborgen.", + // "admin.registries.bitstream-formats.edit.internal.label": "Internal", "admin.registries.bitstream-formats.edit.internal.label": "Intern", + // "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", "admin.registries.bitstream-formats.edit.mimetype.hint": "Der MIME Typ dieses Formates. Er muss nicht einzigartig sein.", + // "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Typ", + // "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", "admin.registries.bitstream-formats.edit.shortDescription.hint": "Ein eindeutiger Name für dieses Format, (z.B. Microsoft Word XP oder Microsoft Word 2000)", + // "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", + // "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", "admin.registries.bitstream-formats.edit.success.content": "Das Dateiformat wurde erfolgreich bearbeitet.", + // "admin.registries.bitstream-formats.edit.success.head": "Success", "admin.registries.bitstream-formats.edit.success.head": "Erfolg", + // "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", "admin.registries.bitstream-formats.edit.supportLevel.hint": "Der Unterstützungsgrad den Ihre Einrichtung für dieses Format anbietet.", + // "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", "admin.registries.bitstream-formats.edit.supportLevel.label": "Unterstützungsgrad", + // "admin.registries.bitstream-formats.head": "Bitstream Format Registry", "admin.registries.bitstream-formats.head": "Referenzliste der Dateiformate", + // "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", "admin.registries.bitstream-formats.no-items": "Es gibt keine Dateiformate in der Referenzliste.", + // "admin.registries.bitstream-formats.table.delete": "Delete selected", "admin.registries.bitstream-formats.table.delete": "Auswahl löschen", + // "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", "admin.registries.bitstream-formats.table.deselect-all": "Alle abwählen", + // "admin.registries.bitstream-formats.table.internal": "internal", "admin.registries.bitstream-formats.table.internal": "intern", + // "admin.registries.bitstream-formats.table.mimetype": "MIME Type", "admin.registries.bitstream-formats.table.mimetype": "MIME Typ", + // "admin.registries.bitstream-formats.table.name": "Name", "admin.registries.bitstream-formats.table.name": "Name", + // "admin.registries.bitstream-formats.table.return": "Return", "admin.registries.bitstream-formats.table.return": "Zurück", + // "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Bekannt", + // "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Unterstützt", + // "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unbekannt", + // "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", "admin.registries.bitstream-formats.table.supportLevel.head": "Unterstützungsgrad", + // "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", "admin.registries.bitstream-formats.title": "DSpace Angular :: Referenzliste der Dateiformate", + + + // "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": "In der Referenzliste der Metadaten sind alle Metadatenfelder aufgeführt, die in diesem Repositorium zur Verfügung stehen. Diese Felder können auf verschiedene Schemata aufgeteilt sein. Für DSpace ist das Dublin Core Schema auf jeden Fall erforderlich.", + // "admin.registries.metadata.form.create": "Create metadata schema", "admin.registries.metadata.form.create": "Metadatenschema anlegen", + // "admin.registries.metadata.form.edit": "Edit metadata schema", "admin.registries.metadata.form.edit": "Metadatenschema bearbeiten", + // "admin.registries.metadata.form.name": "Name", "admin.registries.metadata.form.name": "Name", + // "admin.registries.metadata.form.namespace": "Namespace", "admin.registries.metadata.form.namespace": "Namensraum", + // "admin.registries.metadata.head": "Metadata Registry", "admin.registries.metadata.head": "Metadatenreferenzliste", + // "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", "admin.registries.metadata.schemas.no-items": "Es gibt keinen Metadatenschemata in der Referenzliste.", + // "admin.registries.metadata.schemas.table.delete": "Delete selected", "admin.registries.metadata.schemas.table.delete": "Ausgewählte löschen", + // "admin.registries.metadata.schemas.table.id": "ID", "admin.registries.metadata.schemas.table.id": "ID", + // "admin.registries.metadata.schemas.table.name": "Name", "admin.registries.metadata.schemas.table.name": "Name", + // "admin.registries.metadata.schemas.table.namespace": "Namespace", "admin.registries.metadata.schemas.table.namespace": "Namensraum", + // "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", "admin.registries.metadata.title": "DSpace Angular :: Metadatenreferenzliste", + + + // "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", "admin.registries.schema.description": "Dies ist das Metadatenschema für den Namensraum \"{{namespace}}\".", + // "admin.registries.schema.fields.head": "Schema metadata fields", "admin.registries.schema.fields.head": "Im Schema enthaltene Metadatenfelder", + // "admin.registries.schema.fields.no-items": "No metadata fields to show.", "admin.registries.schema.fields.no-items": "Es gibt keine Metadatenfelder in diesem Schema.", + // "admin.registries.schema.fields.table.delete": "Delete selected", "admin.registries.schema.fields.table.delete": "Ausgewähltes löschen", + // "admin.registries.schema.fields.table.field": "Field", "admin.registries.schema.fields.table.field": "Feld", + // "admin.registries.schema.fields.table.scopenote": "Scope Note", "admin.registries.schema.fields.table.scopenote": "Gültigkeitsbereich", + // "admin.registries.schema.form.create": "Create metadata field", "admin.registries.schema.form.create": "Metadatenfeld anlegen", + // "admin.registries.schema.form.edit": "Edit metadata field", "admin.registries.schema.form.edit": "Metadatenfeld bearbeiten", + // "admin.registries.schema.form.element": "Element", "admin.registries.schema.form.element": "Element", + // "admin.registries.schema.form.qualifier": "Qualifier", "admin.registries.schema.form.qualifier": "Qualifizierer", + // "admin.registries.schema.form.scopenote": "Scope Note", "admin.registries.schema.form.scopenote": "Geltungsbereich", + // "admin.registries.schema.head": "Metadata Schema", "admin.registries.schema.head": "Metadatenschema", + // "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", "admin.registries.schema.notification.created": "\"{{prefix}}\" wurde erfolgreich angelegt.", + // "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", "admin.registries.schema.notification.deleted.failure": "{{amount}} Metadatenschema(ta) konnte(n) nicht gelöscht werden", + // "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", "admin.registries.schema.notification.deleted.success": "{{amount}} Metadatenschema(ta) wurde(n) gelöscht.", + // "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", "admin.registries.schema.notification.edited": "Das Schema \"{{prefix}}\" wurde erfolgreich bearbeitet.", + // "admin.registries.schema.notification.failure": "Error", "admin.registries.schema.notification.failure": "Fehler", + // "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", "admin.registries.schema.notification.field.created": "Das Feld \"{{field}}\" wurde erfolgreich angelegt.", + // "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", "admin.registries.schema.notification.field.deleted.failure": "{{amount}} Metadatenfeld(er) konnte(n) nicht gelöscht werden.", + // "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", "admin.registries.schema.notification.field.deleted.success": "{{amount}} Metadatenfeld(er) wurde(n) erfolgreich gelöscht.", + // "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", "admin.registries.schema.notification.field.edited": "Das Feld \"{{field}}\" wurde erfolgreich bearbeitet.", + // "admin.registries.schema.notification.success": "Success", "admin.registries.schema.notification.success": "Erfolg", + // "admin.registries.schema.return": "Return", "admin.registries.schema.return": "Zurück", + // "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", "admin.registries.schema.title": "DSpace Angular :: Referenzliste der Metadatenschemata", + + + // "auth.errors.invalid-user": "Invalid email address or password.", "auth.errors.invalid-user": "Password oder E-Mail-Adresse ungültig.", + // "auth.messages.expired": "Your session has expired. Please log in again.", "auth.messages.expired": "Ihre Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein.", + + // "browse.comcol.by.author": "By Author", "browse.comcol.by.author": "Nach Autor/in", + // "browse.comcol.by.dateissued": "By Issue Date", "browse.comcol.by.dateissued": "Nach Erscheinungsjahr", + // "browse.comcol.by.subject": "By Subject", "browse.comcol.by.subject": "Nach Schlagwort", + // "browse.comcol.by.title": "By Title", "browse.comcol.by.title": "Nach Titel", + // "browse.comcol.head": "Browse", "browse.comcol.head": "Listen", + // "browse.empty": "No items to show.", "browse.empty": "Es gibt keine Dokumente, die angezeigt werden können.", + // "browse.metadata.author": "Author", "browse.metadata.author": "Autor/in", + // "browse.metadata.dateissued": "Issue Date", "browse.metadata.dateissued": "Erscheinungsdatum", + // "browse.metadata.subject": "Subject", "browse.metadata.subject": "Schlagwort", + // "browse.metadata.title": "Title", "browse.metadata.title": "Titel", + + // "browse.metadata.author.breadcrumbs": "Browse by Author", + "browse.metadata.author.breadcrumbs": "Auflistung nach Autor(in)", + + // "browse.metadata.dateissued.breadcrumbs": "Browse by Date", + "browse.metadata.dateissued.breadcrumbs": "Auflistung nach Datum", + + // "browse.metadata.subject.breadcrumbs": "Browse by Subject", + "browse.metadata.subject.breadcrumbs": "Auflistung nach Schlagwort", + + // "browse.metadata.title.breadcrumbs": "Browse by Title", + "browse.metadata.title.breadcrumbs": "Auflistung nach Titel", + // "browse.startsWith.choose_start": "(Choose start)", "browse.startsWith.choose_start": "(Startpunkt wählen)", + // "browse.startsWith.choose_year": "(Choose year)", "browse.startsWith.choose_year": "(Zeitpunkt wählen)", + // "browse.startsWith.jump": "Jump to a point in the index:", "browse.startsWith.jump": "Zu einem Punkt im Index springen:", + // "browse.startsWith.months.april": "April", "browse.startsWith.months.april": "April", + // "browse.startsWith.months.august": "August", "browse.startsWith.months.august": "August", + // "browse.startsWith.months.december": "December", "browse.startsWith.months.december": "Dezember", + // "browse.startsWith.months.february": "February", "browse.startsWith.months.february": "Februar", + // "browse.startsWith.months.january": "January", "browse.startsWith.months.january": "Januar", + // "browse.startsWith.months.july": "July", "browse.startsWith.months.july": "Juli", + // "browse.startsWith.months.june": "June", "browse.startsWith.months.june": "Juni", + // "browse.startsWith.months.march": "March", "browse.startsWith.months.march": "März", + // "browse.startsWith.months.may": "May", "browse.startsWith.months.may": "Mai", + // "browse.startsWith.months.none": "(Choose month)", "browse.startsWith.months.none": "(Monat auswählen)", + // "browse.startsWith.months.november": "November", "browse.startsWith.months.november": "November", + // "browse.startsWith.months.october": "October", "browse.startsWith.months.october": "Oktober", + // "browse.startsWith.months.september": "September", "browse.startsWith.months.september": "September", + // "browse.startsWith.submit": "Go", "browse.startsWith.submit": "Los", + // "browse.startsWith.type_date": "Or type in a date (year-month):", "browse.startsWith.type_date": "Oder geben Sie ein Datum ein:", + // "browse.startsWith.type_text": "Or enter first few letters:", "browse.startsWith.type_text": "Oder geben Sie die ersten Buchstaben ein:", + // "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", "browse.title": "Auflistung von {{ collection }} nach {{ field }} {{ value }}", - // "chips.remove": "Remove chip", + + // "chips.remove": "Remove chip", "chips.remove": "Teil löschen", + + // "collection.create.head": "Create a Collection", "collection.create.head": "Eine Sammlung anlegen", + + // "collection.create.notifications.success": "Successfully created the Collection", + "collection.create.notifications.success": "Sammlung erfolgreich angelegt", + // "collection.create.sub-head": "Create a Collection for Community {{ parent }}", "collection.create.sub-head": "Eine Sammlung in dem Bereich {{ parent }} anlegen", + // "collection.delete.cancel": "Cancel", "collection.delete.cancel": "Abbrechen", + // "collection.delete.confirm": "Confirm", "collection.delete.confirm": "Bestätigen", + // "collection.delete.head": "Delete Collection", "collection.delete.head": "Sammlung löschen", + // "collection.delete.notification.fail": "Collection could not be deleted", "collection.delete.notification.fail": "Die Sammlung konnte nicht gelöscht werden.", + // "collection.delete.notification.success": "Successfully deleted collection", "collection.delete.notification.success": "Die Sammlung wurde erfolgreich gelöscht", + // "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", "collection.delete.text": "Sind Sie sicher, dass Sie die Sammlung \"{{ dso }}\" löschen wollen?", + + // "collection.edit.delete": "Delete this collection", "collection.edit.delete": "Diese Sammlung löschen", + // "collection.edit.head": "Edit Collection", "collection.edit.head": "Sammlung bearbeiten", + // "collection.edit.breadcrumbs": "Edit Collection", + "collection.edit.breadcrumbs": "Sammlung bearbeiten", + + + // "collection.edit.item-mapper.cancel": "Cancel", "collection.edit.item-mapper.cancel": "Abbrechen", + // "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", "collection.edit.item-mapper.collection": "Sammlung: \"{{name}}\"", + // "collection.edit.item-mapper.confirm": "Map selected items", "collection.edit.item-mapper.confirm": "Ausgewählte Ressourcen spiegeln", + // "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", "collection.edit.item-mapper.description": "Sammlungsadministratoren haben die Möglichkeit Ressourcen von einer Sammlung in eine andere zu spiegeln. Man kann nach Ressourcen in anderen Sammlungen suchen und diese spiegeln oder sich eine Liste der gespiegelten Ressourcen anzeigen lassen.", + // "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", "collection.edit.item-mapper.head": "Ressourcen Spiegeln - Spiegelt Ressourcen aus anderen Sammlungen", + // "collection.edit.item-mapper.no-search": "Please enter a query to search", "collection.edit.item-mapper.no-search": "Bitte geben Sie eine Suchanfrage ein.", + // "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", "collection.edit.item-mapper.notifications.map.error.content": "Beim Spiegeln von {{amount}} Ressource(n) ist ein Fehler aufgetreten.", + // "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", "collection.edit.item-mapper.notifications.map.error.head": "Fehler beim Spiegeln", + // "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", "collection.edit.item-mapper.notifications.map.success.content": "{{amount}} Ressource(n) erfolgreich gespiegelt.", + // "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", "collection.edit.item-mapper.notifications.map.success.head": "Spiegelung abgeschlossen", + // "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", "collection.edit.item-mapper.notifications.unmap.error.content": "Beim Spiegeln von {{amount}} Ressource(n) ist ein Fehler aufgetreten.", + // "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", "collection.edit.item-mapper.notifications.unmap.error.head": "Spiegeln Entfernen Fehler", + // "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", "collection.edit.item-mapper.notifications.unmap.success.content": "{{amount}} Spiegelung(en) wurde(n) entfernt.", + // "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", "collection.edit.item-mapper.notifications.unmap.success.head": "Spiegelung entfernen abgeschlossen", + // "collection.edit.item-mapper.remove": "Remove selected item mappings", "collection.edit.item-mapper.remove": "Spiegelung der gewählten Ressourcen entfernen.", + // "collection.edit.item-mapper.tabs.browse": "Browse mapped items", "collection.edit.item-mapper.tabs.browse": "Gespiegelte Ressourcen auflisten", + // "collection.edit.item-mapper.tabs.map": "Map new items", "collection.edit.item-mapper.tabs.map": "Neue Ressourcen spiegeln", + + + // "collection.edit.logo.label": "Collection logo", + "collection.edit.logo.label": "Sammlungslogo", + + // "collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.", + "collection.edit.logo.notifications.add.error": "Hochladen des Sammlungslogos fehlgeschlagen. Bitte überprüfen Sie den Inhalt, bevor Sie es nochmal versuchen.", + + // "collection.edit.logo.notifications.add.success": "Upload Collection logo successful.", + "collection.edit.logo.notifications.add.success": "Sammlungslogo erfolgreich hochgeladen.", + + // "collection.edit.logo.notifications.delete.success.title": "Logo deleted", + "collection.edit.logo.notifications.delete.success.title": "Logo gelöscht", + + // "collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo", + "collection.edit.logo.notifications.delete.success.content": "Logo der Sammlung erfolgreich gelöscht", + + // "collection.edit.logo.notifications.delete.error.title": "Error deleting logo", + "collection.edit.logo.notifications.delete.error.title": "Fehler beim Löschen des Logos", + + // "collection.edit.logo.upload": "Drop a Collection Logo to upload", + "collection.edit.logo.upload": "Ziehen Sie ein Logo herüber, um es hochzuladen", + + + + // "collection.edit.notifications.success": "Successfully edited the Collection", + "collection.edit.notifications.success": "Sammlung erfolgreich bearbeitet", + + // "collection.edit.return": "Return", + "collection.edit.return": "Zurück", + + + + // "collection.edit.tabs.curate.head": "Curate", + "collection.edit.tabs.curate.head": "Datenpflege", + + // "collection.edit.tabs.curate.title": "Collection Edit - Curate", + "collection.edit.tabs.curate.title": "Sammlung bearbeiten - Datenpflege", + + // "collection.edit.tabs.metadata.head": "Edit Metadata", + "collection.edit.tabs.metadata.head": "Metadaten bearbeiten", + + // "collection.edit.tabs.metadata.title": "Collection Edit - Metadata", + "collection.edit.tabs.metadata.title": "Sammlung bearbeiten - Metadaten", + + // "collection.edit.tabs.roles.head": "Assign Roles", + "collection.edit.tabs.roles.head": "Rollen zuweisen", + + // "collection.edit.tabs.roles.title": "Collection Edit - Roles", + "collection.edit.tabs.roles.title": "Sammlung bearbeiten - Rollen", + + // "collection.edit.tabs.source.external": "This collection harvests its content from an external source", + "collection.edit.tabs.source.external": "Diese Sammlung bezieht ihre Inhalte aus einer externen Quelle", + + // "collection.edit.tabs.source.form.errors.oaiSource.required": "You must provide a set id of the target collection.", + "collection.edit.tabs.source.form.errors.oaiSource.required": "Sie müssen die OAI-Set-Id der Zielsammlung angeben.", + + // "collection.edit.tabs.source.form.harvestType": "Content being harvested", + "collection.edit.tabs.source.form.harvestType": "Der Inhalt wird automatisch bezogen", + + // "collection.edit.tabs.source.form.head": "Configure an external source", + "collection.edit.tabs.source.form.head": "Externe Quelle konfigurieren", + + // "collection.edit.tabs.source.form.metadataConfigId": "Metadata Format", + "collection.edit.tabs.source.form.metadataConfigId": "Metadatenformat", + + // "collection.edit.tabs.source.form.oaiSetId": "OAI specific set id", + "collection.edit.tabs.source.form.oaiSetId": "OAI spezifische Set-ID", + + // "collection.edit.tabs.source.form.oaiSource": "OAI Provider", + "collection.edit.tabs.source.form.oaiSource": "OAI Anbieter", + + // "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_BITSTREAMS": "Harvest metadata and bitstreams (requires ORE support)", + "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_BITSTREAMS": "Bezieht Metadaten und Dateien (ORE-Schnittstelle erforderlich)", + + // "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_REF": "Harvest metadata and references to bitstreams (requires ORE support)", + "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_REF": "Bezieht Metadaten und Referenzen auf Dateien (ORE-Schnittstelle erforderlich))", + + // "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Harvest metadata only", + "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Nur Metadaten beziehen", + + // "collection.edit.tabs.source.head": "Content Source", + "collection.edit.tabs.source.head": "Herkunft des Inhalts", + + // "collection.edit.tabs.source.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + "collection.edit.tabs.source.notifications.discarded.content": "Die Änderungen wurden verworfen. Um die Änderungen wieder einzusetzen klicken Sie auf den 'Rückgängig' Knopf", + + // "collection.edit.tabs.source.notifications.discarded.title": "Changed discarded", + "collection.edit.tabs.source.notifications.discarded.title": "Änderungen verworfen", + + // "collection.edit.tabs.source.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", + "collection.edit.tabs.source.notifications.invalid.content": "Ihre Änderungen wurden nicht gespeichert. Stellen Sie sicher, dass alle Felder gültig sind bevor Sie speichern.", + + // "collection.edit.tabs.source.notifications.invalid.title": "Metadata invalid", + "collection.edit.tabs.source.notifications.invalid.title": "Metadaten ungültig", + + // "collection.edit.tabs.source.notifications.saved.content": "Your changes to this collection's content source were saved.", + "collection.edit.tabs.source.notifications.saved.content": "Ihre Änderunge bezüglich der Quelle für den Inhalt der Sammlung wurden gespeichert.", + + // "collection.edit.tabs.source.notifications.saved.title": "Content Source saved", + "collection.edit.tabs.source.notifications.saved.title": "Bezugsquelle für den Inhalt gespeichert", + + // "collection.edit.tabs.source.title": "Collection Edit - Content Source", + "collection.edit.tabs.source.title": "Sammlung bearbeiten - Bezugsquelle für den Inhalt", + + + // "collection.form.abstract": "Short Description", "collection.form.abstract": "Kurzbeschreibung", + // "collection.form.description": "Introductory text (HTML)", "collection.form.description": "Einleitender Text (HTML)", + // "collection.form.errors.title.required": "Please enter a collection name", "collection.form.errors.title.required": "Bitte geben Sie einen Namen für die Sammlung ein.", + // "collection.form.license": "License", "collection.form.license": "Lizenzbestimmung", + // "collection.form.provenance": "Provenance", "collection.form.provenance": "Herkunft", + // "collection.form.rights": "Copyright text (HTML)", "collection.form.rights": "Copyright Text (HTML)", + // "collection.form.tableofcontents": "News (HTML)", "collection.form.tableofcontents": "Neuigkeiten (HTML)", + // "collection.form.title": "Name", "collection.form.title": "Name", + + // "collection.page.browse.recent.head": "Recent Submissions", "collection.page.browse.recent.head": "Neueste Veröffentlichungen", + // "collection.page.browse.recent.empty": "No items to show", "collection.page.browse.recent.empty": "Es gibt keine Ressourcen zum Anzeigen", + // "collection.page.handle": "Permanent URI for this collection", "collection.page.handle": "Dauerhafte URI für die Sammlung", + // "collection.page.license": "License", "collection.page.license": "Lizenz", + // "collection.page.news": "News", "collection.page.news": "Neuigkeiten", + + // "collection.select.confirm": "Confirm selected", "collection.select.confirm": "Auswahl bestätigen", + // "collection.select.empty": "No collections to show", "collection.select.empty": "Es gibt keine Sammlungen, die angezeigt werden können", + // "collection.select.table.title": "Title", "collection.select.table.title": "Titel", + + + // "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.", + "collection.source.update.notifications.error.content": "Die angegebenen Einstellen wurden getestet und haben nicht funktioniert.", + + // "collection.source.update.notifications.error.title": "Server Error", + "collection.source.update.notifications.error.title": "Serverfehler", + + + + // "communityList.tabTitle": "DSpace - Community List", + "communityList.tabTitle": "DSpace - Bereichsliste", + + // "communityList.title": "List of Communities", + "communityList.title": "Liste der Bereiche", + + // "communityList.showMore": "Show More", + "communityList.showMore": "Mehr anzeigen", + + + // "community.create.head": "Create a Community", "community.create.head": "Sammlung anlegen", + + // "community.create.notifications.success": "Successfully created the Community", + "community.create.notifications.success": "Bereich erfolgreich angelegt", + // "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", "community.create.sub-head": "Teilbeirech im Bereich {{ parent }} anlegen", + // "community.delete.cancel": "Cancel", "community.delete.cancel": "Abbrechen", + // "community.delete.confirm": "Confirm", "community.delete.confirm": "Bestätigen", + // "community.delete.head": "Delete Community", "community.delete.head": "Bereich Löschen", + // "community.delete.notification.fail": "Community could not be deleted", - "community.delete.notification.fail": "Der Bereich konnte nicht gelöscht werden.", + "community.delete.notification.fail": "Der Bereich konnte nicht gelöscht werden.", + // "community.delete.notification.success": "Successfully deleted community", "community.delete.notification.success": "Der Bereich wurde erfolgreich gelöscht.", + // "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", "community.delete.text": "Sind Sie sicher, dass Sie den Bereich \"{{ dso }}\" löschen möchten?", + // "community.edit.delete": "Delete this community", "community.edit.delete": "Diesen Bereich löschen", + // "community.edit.head": "Edit Community", "community.edit.head": "Bereich bearbeiten", + + // "community.edit.breadcrumbs": "Edit Community", + "community.edit.breadcrumbs": "Bereich bearbeiten", + + + // "community.edit.logo.label": "Community logo", + "community.edit.logo.label": "Bereichslogo", + + // "community.edit.logo.notifications.add.error": "Uploading Community logo failed. Please verify the content before retrying.", + "community.edit.logo.notifications.add.error": "Hochladen des Bereichslogos fehlgeschlagen. Bitte überprüfen Sie den Inhalt bevor Sie es noch einmal versuchen.", + + // "community.edit.logo.notifications.add.success": "Upload Community logo successful.", + "community.edit.logo.notifications.add.success": "Hochladen des Bereichslogos erfolgreich.", + + // "community.edit.logo.notifications.delete.success.title": "Logo deleted", + "community.edit.logo.notifications.delete.success.title": "Logo gelöscht", + + // "community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo", + "community.edit.logo.notifications.delete.success.content": "Das Bereichslogo wurde erfolgreich gelöscht", + + // "community.edit.logo.notifications.delete.error.title": "Error deleting logo", + "community.edit.logo.notifications.delete.error.title": "Fehler beim Löschen des Logos", + + // "community.edit.logo.upload": "Drop a Community Logo to upload", + "community.edit.logo.upload": "Ziehen Sie ein Bereichslogo herüber, um es hochzuladen", + + + + // "community.edit.notifications.success": "Successfully edited the Community", + "community.edit.notifications.success": "Bereich erfolgreich bearbeitet", + + // "community.edit.return": "Return", + "community.edit.return": "Zurück", + + + + // "community.edit.tabs.curate.head": "Curate", + "community.edit.tabs.curate.head": "Datenpflege", + + // "community.edit.tabs.curate.title": "Community Edit - Curate", + "community.edit.tabs.curate.title": "Bereich bearbeiten - Datenpflege", + + // "community.edit.tabs.metadata.head": "Edit Metadata", + "community.edit.tabs.metadata.head": "Metadaten bearbeiten", + + // "community.edit.tabs.metadata.title": "Community Edit - Metadata", + "community.edit.tabs.metadata.title": "Bereich bearbeiten - Metadaten", + + // "community.edit.tabs.roles.head": "Assign Roles", + "community.edit.tabs.roles.head": "Rollen zuordnen", + + // "community.edit.tabs.roles.title": "Community Edit - Roles", + "community.edit.tabs.roles.title": "Bereich bearbeiten - Rollen", + + + // "community.form.abstract": "Short Description", "community.form.abstract": "Kurzbeschreibung", + // "community.form.description": "Introductory text (HTML)", "community.form.description": "Einleitender Text (HTML)", + // "community.form.errors.title.required": "Please enter a community name", "community.form.errors.title.required": "Bitte geben Sie einen Namen für den Bereich ein.", + // "community.form.rights": "Copyright text (HTML)", "community.form.rights": "Copyrighterklärung (HTML)", + // "community.form.tableofcontents": "News (HTML)", "community.form.tableofcontents": "Neuigkeiten (HTML)", + // "community.form.title": "Name", "community.form.title": "Name", + // "community.page.handle": "Permanent URI for this community", "community.page.handle": "Dauerhafte URI für den Bereich", + // "community.page.license": "License", "community.page.license": "Lizenz", + // "community.page.news": "News", "community.page.news": "Neuigkeiten", + // "community.all-lists.head": "Subcommunities and Collections", "community.all-lists.head": "Teilbereiche in diesem Bereich", + // "community.sub-collection-list.head": "Collections of this Community", "community.sub-collection-list.head": "Sammlungen in diesem Bereich", + // "community.sub-community-list.head": "Communities of this Community", "community.sub-community-list.head": "Teilbereiche in diesem Bereich", + + // "dso-selector.create.collection.head": "New collection", "dso-selector.create.collection.head": "Neue Sammlung", + // "dso-selector.create.community.head": "New community", "dso-selector.create.community.head": "Neuer Bereich", + // "dso-selector.create.community.sub-level": "Create a new community in", "dso-selector.create.community.sub-level": "Einen neuen Bereich anlegen in", + // "dso-selector.create.community.top-level": "Create a new top-level community", "dso-selector.create.community.top-level": "Einen neuen Bereich auf oberster Ebene anlgen", + // "dso-selector.create.item.head": "New item", "dso-selector.create.item.head": "Neue Ressource", + // "dso-selector.edit.collection.head": "Edit collection", "dso-selector.edit.collection.head": "Sammlung bearbeiten", + // "dso-selector.edit.community.head": "Edit community", "dso-selector.edit.community.head": "Bereich bearbeiten", + // "dso-selector.edit.item.head": "Edit item", "dso-selector.edit.item.head": "Ressource bearbeiten", + // "dso-selector.no-results": "No {{ type }} found", "dso-selector.no-results": "Kein(e) {{ type }} gefunden", + // "dso-selector.placeholder": "Search for a {{ type }}", "dso-selector.placeholder": "Suche nach {{ type }}", + + // "error.browse-by": "Error fetching items", "error.browse-by": "Fehler beim Laden der Ressourcen", + // "error.collection": "Error fetching collection", "error.collection": "Fehler beim Laden der Sammlung", + // "error.collections": "Error fetching collections", "error.collections": "Fehler beim Laden der Sammlungen", + // "error.community": "Error fetching community", "error.community": "Fehler beim Laden des Bereichs", + // "error.identifier": "No item found for the identifier", "error.identifier": "Zu dieser Kennung wurde keine Ressource gefunden", + // "error.default": "Error", "error.default": "Fehler", + // "error.item": "Error fetching item", "error.item": "Fehler beim Laden der Ressource", + // "error.items": "Error fetching items", - "error.items": "Fejöer beim Laden der Ressorucen", + "error.items": "Fejöer beim Laden der Ressourcen", + // "error.objects": "Error fetching objects", "error.objects": "Fehler beim Laden der Objekte", + // "error.recent-submissions": "Error fetching recent submissions", "error.recent-submissions": "Fehler beim Laden der aktuellsten Veröffentlichungen", + // "error.search-results": "Error fetching search results", - "error.search-results": "Fehler beim Laden der Suchergebenisse", + "error.search-results": "Fehler beim Laden der Suchergebnisse", + // "error.sub-collections": "Error fetching sub-collections", "error.sub-collections": "Fehler beim Laden der Teilsammlungen", + // "error.sub-communities": "Error fetching sub-communities", "error.sub-communities": "Fehler beim Laden der Teilbereiche", + // "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

", "error.submission.sections.init-form-error": "Ein Fehler ist bei der Initialisierung der Eingabemaske aufgetreten. Bitte Überprüfen Sie die Konfiguration Ihrer Eingabemaske. Details s.u.

", + // "error.top-level-communities": "Error fetching top-level communities", "error.top-level-communities": "Hauptbereich konnte nicht geladen werden", + // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", "error.validation.license.notgranted": "Um die Veröffentlichung abzuschließen, müssen Sie die Lizenzbedingungen akzeptieren. Wenn Sie zur Zeit dazu nicht in der Lage sind, können Sie Ihre Arbeit sichern und später dazu zurückgkehren, um zuzustimmen oder die Einreichung zu löschen.", + // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", "error.validation.pattern": "Die Eingabe hat dem folgenden Muster zu entsprechen: {{ pattern }}.", + // "error.validation.filerequired": "The file upload is mandatory", + "error.validation.filerequired": "Das Hochladen einer Datei ist erforderlich.", + + + // "footer.copyright": "copyright © 2002-{{ year }}", "footer.copyright": "Copyright © 2002-{{ year }}", + // "footer.link.dspace": "DSpace software", - "footer.link.dspace": "DSpace software", + "footer.link.dspace": "DSpace Software", + // "footer.link.duraspace": "DuraSpace", "footer.link.duraspace": "DuraSpace", + + // "form.add": "Add", + "form.add": "Hinzufügen", + + // "form.add-help": "Click here to add the current entry and to add another one", + "form.add-help": "Klicken Sie hier, um den aktuellen Eintrag und einen weiteren hinzuzufügen", + // "form.cancel": "Cancel", "form.cancel": "Abbrechen", + // "form.clear": "Clear", "form.clear": "Eingaben löschen", + // "form.clear-help": "Click here to remove the selected value", "form.clear-help": "Klicken Sie hier, um den ausgewählten Wert zu entfernen", + // "form.edit": "Edit", "form.edit": "Bearbeiten", + // "form.edit-help": "Click here to edit the selected value", "form.edit-help": "Klicken Sie hier, um den ausgwählten Wert zu bearbeiten", + // "form.first-name": "First name", "form.first-name": "Vorname", + // "form.group-collapse": "Collapse", "form.group-collapse": "Zusammenklappen", + // "form.group-collapse-help": "Click here to collapse", "form.group-collapse-help": "Zum Zusammenfalten bitte hier klicken", + // "form.group-expand": "Expand", "form.group-expand": "Auffalten", + // "form.group-expand-help": "Click here to expand and add more elements", "form.group-expand-help": "Zum Ausklappten und Hinzufügen von mehr Elementen, klicken Sie hier", + // "form.last-name": "Last name", "form.last-name": "Nachname", + // "form.loading": "Loading...", "form.loading": "Am Laden ...", + + // "form.lookup": "Lookup", + "form.lookup": "Nachschlagen", + + // "form.lookup-help": "Click here to look up an existing relation", + "form.lookup-help": "Klicken Sie hier, um eine bestehende Beziehung nachzuschlagen", + // "form.no-results": "No results found", "form.no-results": "Keine Ergebnisse gefunden", + // "form.no-value": "No value entered", "form.no-value": "Kein Wert eingegeben", + // "form.other-information": {}, "form.other-information": {}, + // "form.remove": "Remove", "form.remove": "Entfernen", + // "form.save": "Save", "form.save": "Speichern", + // "form.save-help": "Save changes", "form.save-help": "Änderungen speichern", + // "form.search": "Search", "form.search": "Suche", + // "form.search-help": "Click here to looking for an existing correspondence", "form.search-help": "Klicken Sie hier, um eine Übereinstimmung zu suchen", + // "form.submit": "Submit", "form.submit": "Abschicken", + + // "home.description": "", "home.description": "", + // "home.title": "DSpace Angular :: Home", "home.title": "DSpace Angular :: Startseite", + // "home.top-level-communities.head": "Communities in DSpace", "home.top-level-communities.head": "Hauptbereiche in DSpace", + // "home.top-level-communities.help": "Select a community to browse its collections.", "home.top-level-communities.help": "Wählen Sie einen Bereiche, um dessen Inhalt anzusehen.", + + // "item.edit.delete.cancel": "Cancel", "item.edit.delete.cancel": "Abbrechen", + // "item.edit.delete.confirm": "Delete", "item.edit.delete.confirm": "Löschen", + // "item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.", "item.edit.delete.description": "Sind Sie sicher, dass diese Ressource komplett gelöscht werden soll. Warnung: Zur Zeit wird kein Grabstein hinterlassen", + // "item.edit.delete.error": "An error occurred while deleting the item", "item.edit.delete.error": "Beim Löschen der Ressource ist ein Fehler aufgetreten", + // "item.edit.delete.header": "Delete item: {{ id }}", "item.edit.delete.header": "Löschen der Ressource: {{ id }}", + // "item.edit.delete.success": "The item has been deleted", "item.edit.delete.success": "Die Ressource wurde gelöscht", + // "item.edit.head": "Edit Item", "item.edit.head": "Ressource bearbeiten", + // "item.edit.breadcrumbs": "Edit Item", + "item.edit.breadcrumbs": "Ressource bearbeiten", + + + // "item.edit.item-mapper.buttons.add": "Map item to selected collections", "item.edit.item-mapper.buttons.add": "Ressource in die gewählte(n) Sammlung(en) spiegeln", + // "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", "item.edit.item-mapper.buttons.remove": "Spiegelung der Ressource aus der/den gewählten Sammlung(en) entfernen", + // "item.edit.item-mapper.cancel": "Cancel", "item.edit.item-mapper.cancel": "Abbrechen", + // "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", "item.edit.item-mapper.description": "Sammlungsadministratoren haben die Möglichkeit Ressourcen von einer Sammlung in eine andere zu spiegeln. Man kann nach Ressourcen in anderen Sammlungen suchen und diese spiegeln oder sich eine Liste der gespiegelten Ressourcen anzeigen lassen.", + // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", "item.edit.item-mapper.head": "Ressourcen Spiegeln - Spiegelt eine Resource in andere Sammlungen", + // "item.edit.item-mapper.item": "Item: \"{{name}}\"", "item.edit.item-mapper.item": "Ressource: \"{{name}}\"", + // "item.edit.item-mapper.no-search": "Please enter a query to search", "item.edit.item-mapper.no-search": "Bitte geben Sie einen Suchbegriff ein", + // "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", "item.edit.item-mapper.notifications.add.error.content": "Beim Spiegeln der Resource in {{amount}} Sammlung(en) ist ein Fehler aufgetreten.", + // "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", "item.edit.item-mapper.notifications.add.error.head": "Fehler beim Spiegeln", + // "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", "item.edit.item-mapper.notifications.add.success.content": "Ressource erfolgreich in {{amount}} Sammlung(en) gespiegelt.", + // "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", "item.edit.item-mapper.notifications.add.success.head": "Spiegeln abgeschlossen", + // "item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.", "item.edit.item-mapper.notifications.remove.error.content": "Beim Entfernen der Spiegelung in {{amount}} Sammlung(en) ist ein Fehler aufgetreten.", + // "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", "item.edit.item-mapper.notifications.remove.error.head": "Fehler beim Entfernen von gespiegelten Ressourcen", + // "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", "item.edit.item-mapper.notifications.remove.success.content": "Spiegelung der Ressource aus {{amount}} Sammlung(en) erfolgreich.", + // "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", "item.edit.item-mapper.notifications.remove.success.head": "Entfernen der Spiegelung abgeschlossen", + // "item.edit.item-mapper.tabs.browse": "Browse mapped collections", "item.edit.item-mapper.tabs.browse": "Gespiegelte Sammlungen anzeigen", + // "item.edit.item-mapper.tabs.map": "Map new collections", "item.edit.item-mapper.tabs.map": "Neue Sammlungen spiegeln", + + // "item.edit.metadata.add-button": "Add", "item.edit.metadata.add-button": "Hinzufügen", + // "item.edit.metadata.discard-button": "Discard", "item.edit.metadata.discard-button": "Verwerfen", + // "item.edit.metadata.edit.buttons.edit": "Edit", "item.edit.metadata.edit.buttons.edit": "Bearbeiten", + // "item.edit.metadata.edit.buttons.remove": "Remove", "item.edit.metadata.edit.buttons.remove": "Entfernen", + // "item.edit.metadata.edit.buttons.undo": "Undo changes", "item.edit.metadata.edit.buttons.undo": "Änderungen rückgängig machen", + // "item.edit.metadata.edit.buttons.unedit": "Stop editing", "item.edit.metadata.edit.buttons.unedit": "Bearbeitung beenden", + // "item.edit.metadata.headers.edit": "Edit", "item.edit.metadata.headers.edit": "Bearbeiten", + // "item.edit.metadata.headers.field": "Field", "item.edit.metadata.headers.field": "Feld", + // "item.edit.metadata.headers.language": "Lang", "item.edit.metadata.headers.language": "Sprache", + // "item.edit.metadata.headers.value": "Value", "item.edit.metadata.headers.value": "Wert", + // "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", "item.edit.metadata.metadatafield.invalid": "Bitte wählen Sie ein gültiges Metadatenfeld", + // "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.metadata.notifications.discarded.content": "Ihre Änderungen wurden verworfen. Um diese wieder anzuwenden, klicken Sie auf den 'Rückgängig machen' Knopf", + // "item.edit.metadata.notifications.discarded.title": "Changed discarded", "item.edit.metadata.notifications.discarded.title": "Änderungen verworfen", + // "item.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", "item.edit.metadata.notifications.invalid.content": "Ihre Änderungen wurden nicht gespeichert. Stellen Sie sicher, dass alle Felder gültig sind, bevor Sie Abspeichern.", + // "item.edit.metadata.notifications.invalid.title": "Metadata invalid", "item.edit.metadata.notifications.invalid.title": "Metadaten ungültig", + // "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", "item.edit.metadata.notifications.outdated.content": "Die Ressource, an der Sie gerade arbeiten, wurde von einem anderen Benutzer geändert. Ihre aktuellen Änderungen werden nicht angewandt, um Konflikte zu vermeiden.", + // "item.edit.metadata.notifications.outdated.title": "Changed outdated", "item.edit.metadata.notifications.outdated.title": "Änderung veraltet", + // "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", "item.edit.metadata.notifications.saved.content": "Ihre Änderungen an den Metadaten der Ressource wurden gespeichert.", + // "item.edit.metadata.notifications.saved.title": "Metadata saved", "item.edit.metadata.notifications.saved.title": "Metadaten gespeichert", + // "item.edit.metadata.reinstate-button": "Undo", "item.edit.metadata.reinstate-button": "Rückgängig machen", + // "item.edit.metadata.save-button": "Save", "item.edit.metadata.save-button": "Speichern", + + // "item.edit.modify.overview.field": "Field", "item.edit.modify.overview.field": "Feld", + // "item.edit.modify.overview.language": "Language", "item.edit.modify.overview.language": "Sprache", + // "item.edit.modify.overview.value": "Value", "item.edit.modify.overview.value": "Wert", + + // "item.edit.move.cancel": "Cancel", "item.edit.move.cancel": "Abbrechen", + // "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", + // "item.edit.move.error": "An error occurred when attempting to move the item", "item.edit.move.error": "Ein Fehler ist beim Verschieben der Ressource aufgetreten", + // "item.edit.move.head": "Move item: {{id}}", "item.edit.move.head": "Ressource verschieben: {{id}}", + // "item.edit.move.inheritpolicies.checkbox": "Inherit policies", "item.edit.move.inheritpolicies.checkbox": "Rechte erben", + // "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", "item.edit.move.inheritpolicies.description": "Standardrechte der Zielsammlung erben", + // "item.edit.move.move": "Move", "item.edit.move.move": "Verschieben", + // "item.edit.move.processing": "Moving...", "item.edit.move.processing": "Verschieben...", + // "item.edit.move.search.placeholder": "Enter a search query to look for collections", "item.edit.move.search.placeholder": "Geben Sie einen Begriff ein, um nach Sammlungen zu suchen", + // "item.edit.move.success": "The item has been moved successfully", "item.edit.move.success": "Die Ressource wurde erfolgreich verschoben", + // "item.edit.move.title": "Move item", "item.edit.move.title": "Ressource verschieben", + + // "item.edit.private.cancel": "Cancel", "item.edit.private.cancel": "Abbrechen", + // "item.edit.private.confirm": "Make it Private", "item.edit.private.confirm": "Privat machen", + // "item.edit.private.description": "Are you sure this item should be made private in the archive?", "item.edit.private.description": "Wollen Sie diese Ressource als privat markieren.", + // "item.edit.private.error": "An error occurred while making the item private", "item.edit.private.error": "Ein Fehler ist aufgetreten bei dem Versuch, die Ressource privat zu machen", + // "item.edit.private.header": "Make item private: {{ id }}", "item.edit.private.header": "Ressource: {{ id }} privat machen", + // "item.edit.private.success": "The item is now private", "item.edit.private.success": "Diese Ressource ist nun privat", + + // "item.edit.public.cancel": "Cancel", "item.edit.public.cancel": "Abbrechen", + // "item.edit.public.confirm": "Make it Public", "item.edit.public.confirm": "Öffentlich machen", + // "item.edit.public.description": "Are you sure this item should be made public in the archive?", "item.edit.public.description": "Sind Sie sicher, dasss diese Ressource im Repositorium öffentlich gemacht werden soll?", + // "item.edit.public.error": "An error occurred while making the item public", "item.edit.public.error": "Ein Fehler ist aufgetreten, als die Ressource öffentlich gemacht werden sollte.", + // "item.edit.public.header": "Make item public: {{ id }}", "item.edit.public.header": "Ressource: {{ id }} öffentlich machen", + // "item.edit.public.success": "The item is now public", "item.edit.public.success": "Die Ressource ist nun öffentlich", + + // "item.edit.reinstate.cancel": "Cancel", "item.edit.reinstate.cancel": "Abbrechen", + // "item.edit.reinstate.confirm": "Reinstate", "item.edit.reinstate.confirm": "Reinstantiieren", + // "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", "item.edit.reinstate.description": "Sind Sie sicher, dass die Ressource reinstantiiert werden soll?", + // "item.edit.reinstate.error": "An error occurred while reinstating the item", "item.edit.reinstate.error": "Ein Fehler ist bei der Reinstantiierung der Ressource aufgetreten.", + // "item.edit.reinstate.header": "Reinstate item: {{ id }}", "item.edit.reinstate.header": "Ressource: {{ id }} reinstantiieren", + // "item.edit.reinstate.success": "The item was reinstated successfully", "item.edit.reinstate.success": "Die Ressource wurde erfolgreich reinstantiiert", + + // "item.edit.relationships.discard-button": "Discard", "item.edit.relationships.discard-button": "Abbrechen", + // "item.edit.relationships.edit.buttons.remove": "Remove", "item.edit.relationships.edit.buttons.remove": "Entfernen", + // "item.edit.relationships.edit.buttons.undo": "Undo changes", "item.edit.relationships.edit.buttons.undo": "Rückgängig machen", + // "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.relationships.notifications.discarded.content": "Ihre Änderungen wurden verworfen. Um sie dennoch anzuwenden klicken Sie auf den 'Rückgängig machen' Knopf", + // "item.edit.relationships.notifications.discarded.title": "Changes discarded", "item.edit.relationships.notifications.discarded.title": "Änderungen verworfen", + // "item.edit.relationships.notifications.failed.title": "Error deleting relationship", "item.edit.relationships.notifications.failed.title": "Fehler beim Löschen der Beziehung", + // "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", "item.edit.relationships.notifications.outdated.content": "Die Ressource, die Sie gerade bearbeiten, wurde von einem anderen Benutzer geändert. Ihre aktuellen Änderungen werden verworfen, um Konflikte zu vermeiden.", + // "item.edit.relationships.notifications.outdated.title": "Changes outdated", "item.edit.relationships.notifications.outdated.title": "Änderungen veraltet", + // "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", "item.edit.relationships.notifications.saved.content": "Ihre Änderungen an den Beziehungen der Ressource wurden gespeichert.", + // "item.edit.relationships.notifications.saved.title": "Relationships saved", "item.edit.relationships.notifications.saved.title": "Beziehungen gespeichert", + // "item.edit.relationships.reinstate-button": "Undo", "item.edit.relationships.reinstate-button": "Rückgängig machen", + // "item.edit.relationships.save-button": "Save", "item.edit.relationships.save-button": "Speichern", + + // "item.edit.tabs.bitstreams.head": "Item Bitstreams", "item.edit.tabs.bitstreams.head": "Dateien zur Ressource", + // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", "item.edit.tabs.bitstreams.title": "Ressource bearbeiten - Dateien", + // "item.edit.tabs.curate.head": "Curate", "item.edit.tabs.curate.head": "Pflegen", + // "item.edit.tabs.curate.title": "Item Edit - Curate", "item.edit.tabs.curate.title": "Ressource bearbeiten - Pflegen", + // "item.edit.tabs.metadata.head": "Item Metadata", "item.edit.tabs.metadata.head": "Metadaten", + // "item.edit.tabs.metadata.title": "Item Edit - Metadata", "item.edit.tabs.metadata.title": "Ressource bearbeiten - Metadaten", + // "item.edit.tabs.relationships.head": "Item Relationships", "item.edit.tabs.relationships.head": "Relationen der Ressource", + // "item.edit.tabs.relationships.title": "Item Edit - Relationships", "item.edit.tabs.relationships.title": "Ressource bearbeiten - Relationen", + // "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", "item.edit.tabs.status.buttons.authorizations.button": "Rechte...", + // "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", "item.edit.tabs.status.buttons.authorizations.label": "Rechte der Ressource bearbeiten", + // "item.edit.tabs.status.buttons.delete.button": "Permanently delete", "item.edit.tabs.status.buttons.delete.button": "Endgültig löschen", + // "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", "item.edit.tabs.status.buttons.delete.label": "Unwiderruflich löschen", + // "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", "item.edit.tabs.status.buttons.mappedCollections.button": "Gespiegelte Sammlungen", + // "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", "item.edit.tabs.status.buttons.mappedCollections.label": "Gespiegelte Sammlungen bearbeiten", + // "item.edit.tabs.status.buttons.move.button": "Move...", "item.edit.tabs.status.buttons.move.button": "Verschieben...", + // "item.edit.tabs.status.buttons.move.label": "Move item to another collection", "item.edit.tabs.status.buttons.move.label": "Ressource in eine andere Sammlung verschieben", + // "item.edit.tabs.status.buttons.private.button": "Make it private...", "item.edit.tabs.status.buttons.private.button": "Privat machen...", + // "item.edit.tabs.status.buttons.private.label": "Make item private", - "item.edit.tabs.status.buttons.private.label": "Ressoruce privat machen", + "item.edit.tabs.status.buttons.private.label": "Ressource privat machen", + // "item.edit.tabs.status.buttons.public.button": "Make it public...", "item.edit.tabs.status.buttons.public.button": "Öffentlich machen...", + // "item.edit.tabs.status.buttons.public.label": "Make item public", "item.edit.tabs.status.buttons.public.label": "Ressource öffentlich machen", + // "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", "item.edit.tabs.status.buttons.reinstate.button": "Reinstantiieren...", + // "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", "item.edit.tabs.status.buttons.reinstate.label": "Ressource wieder ins Repositorium einsetzen", + // "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", "item.edit.tabs.status.buttons.withdraw.button": "Zurückziehen...", + // "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", "item.edit.tabs.status.buttons.withdraw.label": "Ressource aus dem Repositorium zurückziehen", + // "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", "item.edit.tabs.status.description": "Ressource bearbeiten. Von hier können Sie Ressourcen zurückziehen, wiedereinsetzen, verschieben oder Löschen. Des weiteren können die Metadaten und die zugehörigen Dateien bearbeitet werden.", + // "item.edit.tabs.status.head": "Item Status", "item.edit.tabs.status.head": "Status der Ressource", + // "item.edit.tabs.status.labels.handle": "Handle", "item.edit.tabs.status.labels.handle": "Handle", + // "item.edit.tabs.status.labels.id": "Item Internal ID", "item.edit.tabs.status.labels.id": "Interne ID der Ressource", + // "item.edit.tabs.status.labels.itemPage": "Item Page", "item.edit.tabs.status.labels.itemPage": "Startseite der Ressource", + // "item.edit.tabs.status.labels.lastModified": "Last Modified", "item.edit.tabs.status.labels.lastModified": "Zuletzt geändert", + // "item.edit.tabs.status.title": "Item Edit - Status", "item.edit.tabs.status.title": "Ressource Bearbeiten - Status", + // "item.edit.tabs.view.head": "View Item", "item.edit.tabs.view.head": "Ressource Ansehen", - // "item.edit.tabs.view.title": "Ressource bearbeiten - Ansicht", + + // "item.edit.tabs.view.title": "Item Edit - View", + "item.edit.tabs.view.title": "Ressource bearbeiten - Ansicht", + + // "item.edit.withdraw.cancel": "Cancel", "item.edit.withdraw.cancel": "Abbrechen", + // "item.edit.withdraw.confirm": "Withdraw", "item.edit.withdraw.confirm": "Zurückziehen", + // "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", "item.edit.withdraw.description": "Sind Sie sicher, dass Sie diese Ressource aus dem Repositorium zurückziehen wollen?", + // "item.edit.withdraw.error": "An error occurred while withdrawing the item", "item.edit.withdraw.error": "Ein Fehler ist beim Zurückziehen der Ressource aufgetreten", + // "item.edit.withdraw.header": "Withdraw item: {{ id }}", "item.edit.withdraw.header": "Ressource: {{ id }} zurückziehen", + // "item.edit.withdraw.success": "The item was withdrawn successfully", "item.edit.withdraw.success": "Die Ressource wurde erfolgreich zurückgezogen", + + // "item.page.abstract": "Abstract", "item.page.abstract": "Zusammenfassung", + // "item.page.author": "Authors", "item.page.author": "AutorInnen", + // "item.page.citation": "Citation", "item.page.citation": "Zitierform", + // "item.page.collections": "Collections", "item.page.collections": "Sammlungen", + // "item.page.date": "Date", "item.page.date": "Datum", + // "item.page.files": "Files", "item.page.files": "Dateien", + // "item.page.filesection.description": "Description:", "item.page.filesection.description": "Beschreibung:", + // "item.page.filesection.download": "Download", "item.page.filesection.download": "Herunterladen", + // "item.page.filesection.format": "Format:", "item.page.filesection.format": "Format:", + // "item.page.filesection.name": "Name:", "item.page.filesection.name": "Name:", + // "item.page.filesection.size": "Size:", "item.page.filesection.size": "Größe:", + // "item.page.journal.search.title": "Articles in this journal", "item.page.journal.search.title": "Artikel in dieser Zeitschrift", + // "item.page.link.full": "Full item page", "item.page.link.full": "Komplettanzeige", + // "item.page.link.simple": "Simple item page", "item.page.link.simple": "Kurzanzeige", + // "item.page.person.search.title": "Articles by this author", "item.page.person.search.title": "Veröffentlichungen dieses/r Autor/in", - // "item.page.related-items.view-more": "View more", - "item.page.related-items.view-more": "Mehr anzeigen", - // "item.page.related-items.view-less": "View less", - "item.page.related-items.view-less": "Weniger anzeigen", + + // "item.page.related-items.view-more": "Show {{ amount }} more", + "item.page.related-items.view-more": "{{ amount }} mehr anzeigen", + + // "item.page.related-items.view-less": "Hide last {{ amount }}", + "item.page.related-items.view-less": "{{ amount }} weniger anzeigen", + + // "item.page.relationships.isAuthorOfPublication": "Publications", + "item.page.relationships.isAuthorOfPublication": "Veröffentichungen", + + // "item.page.relationships.isJournalOfPublication": "Publications", + "item.page.relationships.isJournalOfPublication": "Veröffentlichungen", + + // "item.page.relationships.isOrgUnitOfPerson": "Authors", + "item.page.relationships.isOrgUnitOfPerson": "AutorInnen", + + // "item.page.relationships.isOrgUnitOfProject": "Research Projects", + "item.page.relationships.isOrgUnitOfProject": "Forschungsprojekte", + // "item.page.subject": "Keywords", "item.page.subject": "Schlagwörter", + // "item.page.uri": "URI", "item.page.uri": "URI", + + // "item.select.confirm": "Confirm selected", "item.select.confirm": "Auswahl bestätigen", + // "item.select.empty": "No items to show", "item.select.empty": "Es gibt keine Ressourcen dazu", + // "item.select.table.author": "Author", "item.select.table.author": "Autor/in", + // "item.select.table.collection": "Collection", "item.select.table.collection": "Sammlung", + // "item.select.table.title": "Title", "item.select.table.title": "Titel", + + // "journal.listelement.badge": "Journal", "journal.listelement.badge": "Zeitschrift", + // "journal.page.description": "Description", "journal.page.description": "Beschreibung", + // "journal.page.editor": "Editor-in-Chief", "journal.page.editor": "Chefredakteur", + // "journal.page.issn": "ISSN", "journal.page.issn": "ISSN", + // "journal.page.publisher": "Publisher", "journal.page.publisher": "Verlag", + // "journal.page.titleprefix": "Journal: ", "journal.page.titleprefix": "Zeitschrift: ", + // "journal.search.results.head": "Journal Search Results", "journal.search.results.head": "Zeitschrift Suchergebenisse", + // "journal.search.title": "DSpace Angular :: Journal Search", "journal.search.title": "DSpace Angular :: Zeitschriftensuche", + + // "journalissue.listelement.badge": "Journal Issue", "journalissue.listelement.badge": "Zeitschriftenausgabe", + // "journalissue.page.description": "Description", "journalissue.page.description": "Beschreibeung", + // "journalissue.page.issuedate": "Issue Date", "journalissue.page.issuedate": "Erscheinungsdatum", + // "journalissue.page.journal-issn": "Journal ISSN", "journalissue.page.journal-issn": "ISSN der Zeitschrift", + // "journalissue.page.journal-title": "Journal Title", "journalissue.page.journal-title": "Titel der Zeitschrift", + // "journalissue.page.keyword": "Keywords", "journalissue.page.keyword": "Schlagwörter", + // "journalissue.page.number": "Number", "journalissue.page.number": "Zählung", + // "journalissue.page.titleprefix": "Journal Issue: ", "journalissue.page.titleprefix": "Zeitschriftenausgabe: ", + + // "journalvolume.listelement.badge": "Journal Volume", "journalvolume.listelement.badge": "Zeitschriftenband", + // "journalvolume.page.description": "Description", "journalvolume.page.description": "Beschreibung", + // "journalvolume.page.issuedate": "Issue Date", "journalvolume.page.issuedate": "Erscheinungsdatum", + // "journalvolume.page.titleprefix": "Journal Volume: ", "journalvolume.page.titleprefix": "Zeitschriftenband: ", + // "journalvolume.page.volume": "Volume", "journalvolume.page.volume": "Band", + + // "loading.browse-by": "Loading items...", "loading.browse-by": "Ressourcen am laden...", + // "loading.browse-by-page": "Loading page...", "loading.browse-by-page": "Seite am Laden...", + // "loading.collection": "Loading collection...", "loading.collection": "Sammlung am Laden...", + // "loading.collections": "Loading collections...", "loading.collections": "Sammlungen am Laden...", + + // "loading.content-source": "Loading content source...", + "loading.content-source": "Laden der Bezugsquelle...", + // "loading.community": "Loading community...", "loading.community": "Bereich am Laden...", + // "loading.default": "Loading...", "loading.default": "Am Laden...", + // "loading.item": "Loading item...", "loading.item": "Ressource am Laden...", + // "loading.items": "Loading items...", "loading.items": "Ressourcen am Laden...", + // "loading.mydspace-results": "Loading items...", "loading.mydspace-results": "Ressourcen am Laden...", + // "loading.objects": "Loading...", "loading.objects": "Am Laden...", + // "loading.recent-submissions": "Loading recent submissions...", "loading.recent-submissions": "Aktuellste Veröffentlichungen am Laden...", + // "loading.search-results": "Loading search results...", "loading.search-results": "Suchergebnisse am Laden...", + // "loading.sub-collections": "Loading sub-collections...", "loading.sub-collections": "Untersammlungen am Laden...", + // "loading.sub-communities": "Loading sub-communities...", "loading.sub-communities": "Teilbereiche am Laden...", + // "loading.top-level-communities": "Loading top-level communities...", "loading.top-level-communities": "Hauptbereiche am Laden...", + + // "login.form.email": "Email address", "login.form.email": "E-Mail-Adresse", + // "login.form.forgot-password": "Have you forgotten your password?", "login.form.forgot-password": "Haben Sie Ihr Passwort vergessen?", + // "login.form.header": "Please log in to DSpace", "login.form.header": "Bitte loggen Sie sich ein.", + // "login.form.new-user": "New user? Click here to register.", "login.form.new-user": "Neu hier? Klicken Sie hier, um sich zu registrieren.", + // "login.form.password": "Password", "login.form.password": "Passwort", + // "login.form.submit": "Log in", "login.form.submit": "Einloggen", + // "login.title": "Login", "login.title": "Einloggen", + // "login.breadcrumbs": "Login", + "login.breadcrumbs": "Einloggen", + + + // "logout.form.header": "Log out from DSpace", "logout.form.header": "Aus dem Repositorium abmelden", + // "logout.form.submit": "Log out", "logout.form.submit": "Abmelden", + // "logout.title": "Logout", "logout.title": "Abmelden", + + // "menu.header.admin": "Admin", "menu.header.admin": "Administration", + // "menu.header.image.logo": "Repository logo", "menu.header.image.logo": "Logo des Repositorium", + + // "menu.section.access_control": "Access Control", "menu.section.access_control": "Zugriffskontrolle", + // "menu.section.access_control_authorizations": "Authorizations", "menu.section.access_control_authorizations": "Rechte", + // "menu.section.access_control_groups": "Groups", "menu.section.access_control_groups": "Gruppen", + // "menu.section.access_control_people": "People", "menu.section.access_control_people": "Personen", + + // "menu.section.browse_community": "This Community", "menu.section.browse_community": "Dieser Bereich", + // "menu.section.browse_community_by_author": "By Author", "menu.section.browse_community_by_author": "Nach Autor/in", + // "menu.section.browse_community_by_issue_date": "By Issue Date", "menu.section.browse_community_by_issue_date": "Nach Erscheinungsdateum", + // "menu.section.browse_community_by_title": "By Title", "menu.section.browse_community_by_title": "Nach Titel", + // "menu.section.browse_global": "All of DSpace", "menu.section.browse_global": "Das gesamte Repositorium", + // "menu.section.browse_global_by_author": "By Author", "menu.section.browse_global_by_author": "Nach Autor/in", + // "menu.section.browse_global_by_dateissued": "By Issue Date", "menu.section.browse_global_by_dateissued": "Nach Erscheinungsjahr", + // "menu.section.browse_global_by_subject": "By Subject", "menu.section.browse_global_by_subject": "Nach Schlagwort", + // "menu.section.browse_global_by_title": "By Title", "menu.section.browse_global_by_title": "Nach Titel", + // "menu.section.browse_global_communities_and_collections": "Communities & Collections", "menu.section.browse_global_communities_and_collections": "Bereiche & Sammlungen", + + // "menu.section.control_panel": "Control Panel", "menu.section.control_panel": "Kontrollfeld", + // "menu.section.curation_task": "Curation Task", "menu.section.curation_task": "Datenpflegeroutinen", + + // "menu.section.edit": "Edit", "menu.section.edit": "Bearbeiten", + // "menu.section.edit_collection": "Collection", "menu.section.edit_collection": "Sammlung", + // "menu.section.edit_community": "Community", "menu.section.edit_community": "Bereich", + // "menu.section.edit_item": "Item", "menu.section.edit_item": "Ressource", + + // "menu.section.export": "Export", "menu.section.export": "Export", + // "menu.section.export_collection": "Collection", "menu.section.export_collection": "Sammlung", + // "menu.section.export_community": "Community", "menu.section.export_community": "Bereich", + // "menu.section.export_item": "Item", - "menu.section.export_item": "Ressoruce", + "menu.section.export_item": "Ressource", + // "menu.section.export_metadata": "Metadata", "menu.section.export_metadata": "Metadaten", + + // "menu.section.find": "Find", "menu.section.find": "Finden", + // "menu.section.find_items": "Items", "menu.section.find_items": "Ressourcen", + // "menu.section.find_private_items": "Private Items", "menu.section.find_private_items": "Private Ressourcen", + // "menu.section.find_withdrawn_items": "Withdrawn Items", "menu.section.find_withdrawn_items": "Zurückgezogene Ressourcen", + + // "menu.section.icon.access_control": "Access Control menu section", "menu.section.icon.access_control": "Menübereich Zugriffskontrolle", + // "menu.section.icon.control_panel": "Control Panel menu section", "menu.section.icon.control_panel": "Menübereich Kontrollfeld", + // "menu.section.icon.curation_task": "Curation Task menu section", "menu.section.icon.curation_task": "Menübereich Datepflegeroutinen", + // "menu.section.icon.edit": "Edit menu section", "menu.section.icon.edit": "Menübereich bearbeiten", + // "menu.section.icon.export": "Export menu section", "menu.section.icon.export": "Menübereich Export", + // "menu.section.icon.find": "Find menu section", "menu.section.icon.find": "Menübereich Suche", + // "menu.section.icon.import": "Import menu section", "menu.section.icon.import": "Menübereich Import", + // "menu.section.icon.new": "New menu section", "menu.section.icon.new": "Neuer Menübereich", + // "menu.section.icon.pin": "Pin sidebar", "menu.section.icon.pin": "Seitenleiste Anheften", + // "menu.section.icon.registries": "Registries menu section", "menu.section.icon.registries": "Menübereich Referenzlisten", + // "menu.section.icon.statistics_task": "Statistics Task menu section", "menu.section.icon.statistics_task": "Menübereich Statistikaufgaben", + // "menu.section.icon.unpin": "Unpin sidebar", "menu.section.icon.unpin": "Seitenbereich Loslösen", + + // "menu.section.import": "Import", "menu.section.import": "Import", + // "menu.section.import_batch": "Batch Import (ZIP)", "menu.section.import_batch": "Import (ZIP)", + // "menu.section.import_metadata": "Metadata", "menu.section.import_metadata": "Metadaten", + + // "menu.section.new": "New", "menu.section.new": "Neu", + // "menu.section.new_collection": "Collection", "menu.section.new_collection": "Sammlung", + // "menu.section.new_community": "Community", "menu.section.new_community": "Bereich", + // "menu.section.new_item": "Item", "menu.section.new_item": "Ressource", + // "menu.section.new_item_version": "Item Version", "menu.section.new_item_version": "Ressourcenversion", + + // "menu.section.pin": "Pin sidebar", "menu.section.pin": "Seitenleiste anheften", + // "menu.section.unpin": "Unpin sidebar", "menu.section.unpin": "Seitenleiste loslösen", + + // "menu.section.registries": "Registries", "menu.section.registries": "Referenzlisten", + // "menu.section.registries_format": "Format", "menu.section.registries_format": "Formate", + // "menu.section.registries_metadata": "Metadata", "menu.section.registries_metadata": "Metadaten", + + // "menu.section.statistics": "Statistics", "menu.section.statistics": "Statistiken", + // "menu.section.statistics_task": "Statistics Task", "menu.section.statistics_task": "Statistikaufgaben", + + // "menu.section.toggle.access_control": "Toggle Access Control section", "menu.section.toggle.access_control": "Bereich Zugriffskontrolle umschalten", + // "menu.section.toggle.control_panel": "Toggle Control Panel section", "menu.section.toggle.control_panel": "Bereich Kontrollfeld umschalten", + // "menu.section.toggle.curation_task": "Toggle Curation Task section", "menu.section.toggle.curation_task": "Bereich Datenpflegeroutinen umschalten", + // "menu.section.toggle.edit": "Toggle Edit section", "menu.section.toggle.edit": "Bereich Bearbeiten umschalten", + // "menu.section.toggle.export": "Toggle Export section", "menu.section.toggle.export": "Bereich Export umschalten", + // "menu.section.toggle.find": "Toggle Find section", "menu.section.toggle.find": "Bereich Suche umschalten", + // "menu.section.toggle.import": "Toggle Import section", "menu.section.toggle.import": "Bereich Import umschalten", + // "menu.section.toggle.new": "Toggle New section", "menu.section.toggle.new": "Neuen Bereich umschalten", + // "menu.section.toggle.registries": "Toggle Registries section", "menu.section.toggle.registries": "Bereich Referenzlisten umschalten", + // "menu.section.toggle.statistics_task": "Toggle Statistics Task section", "menu.section.toggle.statistics_task": "Bereich Statistikaufgaben umschalten", + + // "mydspace.description": "", "mydspace.description": "", + // "mydspace.general.text-here": "HERE", "mydspace.general.text-here": "Hier", + // "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", "mydspace.messages.controller-help": "Wählen Sie diese Option, um dem/derjenigen, die die Ressource eingereicht hat, eine Nachricht zu schicken.", + // "mydspace.messages.description-placeholder": "Insert your message here...", "mydspace.messages.description-placeholder": "Geben Sie Ihre Nachricht hier ein...", + // "mydspace.messages.hide-msg": "Hide message", "mydspace.messages.hide-msg": "Nachricht ausblenden", + // "mydspace.messages.mark-as-read": "Mark as read", "mydspace.messages.mark-as-read": "Als gelesen markieren", + // "mydspace.messages.mark-as-unread": "Mark as unread", "mydspace.messages.mark-as-unread": "Als ungelesen markieren", + // "mydspace.messages.no-content": "No content.", "mydspace.messages.no-content": "Kein Inhalt.", + // "mydspace.messages.no-messages": "No messages yet.", "mydspace.messages.no-messages": "Noch keine Nachrichten.", + // "mydspace.messages.send-btn": "Send", "mydspace.messages.send-btn": "Senden", + // "mydspace.messages.show-msg": "Show message", "mydspace.messages.show-msg": "Nachricht anzeigen", + // "mydspace.messages.subject-placeholder": "Subject...", "mydspace.messages.subject-placeholder": "Betreff...", + // "mydspace.messages.submitter-help": "Select this option to send a message to controller.", "mydspace.messages.submitter-help": "Wählen Sie diese Option, um dem Supervisor eine Nachricht zu schicken.", + // "mydspace.messages.title": "Messages", "mydspace.messages.title": "Nachrichten", + // "mydspace.messages.to": "To", "mydspace.messages.to": "An", + // "mydspace.new-submission": "New submission", "mydspace.new-submission": "Neue Veröffentlichung", + // "mydspace.results.head": "Your submissions", "mydspace.results.head": "Ihre Veröffentlichungen", + // "mydspace.results.no-abstract": "No Abstract", "mydspace.results.no-abstract": "Keine Zusammenfassung", + // "mydspace.results.no-authors": "No Authors", "mydspace.results.no-authors": "Keine AutorInnen", + // "mydspace.results.no-collections": "No Collections", "mydspace.results.no-collections": "Keine Sammlungen", + // "mydspace.results.no-date": "No Date", "mydspace.results.no-date": "Kein Datum", + // "mydspace.results.no-files": "No Files", "mydspace.results.no-files": "Keine Dateien", + // "mydspace.results.no-results": "There were no items to show", "mydspace.results.no-results": "Es gibt keine Ressourcen anzuzeigen", + // "mydspace.results.no-title": "No title", "mydspace.results.no-title": "Kein Titel", + // "mydspace.results.no-uri": "No Uri", "mydspace.results.no-uri": "Keine URI", + // "mydspace.show.workflow": "All tasks", "mydspace.show.workflow": "Alle Aufgaben", + // "mydspace.show.workspace": "Your Submissions", "mydspace.show.workspace": "Ihre Veröffentlichungen", + // "mydspace.status.archived": "Archived", "mydspace.status.archived": "Archiviert", + // "mydspace.status.validation": "Validation", "mydspace.status.validation": "Validierung", + // "mydspace.status.waiting-for-controller": "Waiting for controller", "mydspace.status.waiting-for-controller": "Warten auf die Überprüfung", + // "mydspace.status.workflow": "Workflow", "mydspace.status.workflow": "Geschäftsgang", + // "mydspace.status.workspace": "Workspace", "mydspace.status.workspace": "Arbeitsbereich", + // "mydspace.title": "MyDSpace", "mydspace.title": "Mein DSpace", + // "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", "mydspace.upload.upload-failed": "Fehler beim Anlegen eines neuen Arbeitsbereiches. Bitte überprüfen Sie den Inhalt bevor Sie es nochmal versuchen.", + // "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", "mydspace.upload.upload-multiple-successful": "{{qty}} neue(s) Arbeitsbereichressource(n) angelegt.", + // "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", - "mydspace.upload.upload-successful": "Neue Arbeitsbereichressoruce angelegt. Klicken Sie hier {{here}}, um sie zu bearbeiten.", + "mydspace.upload.upload-successful": "Neue Arbeitsbereichressource angelegt. Klicken Sie hier {{here}}, um sie zu bearbeiten.", + // "mydspace.view-btn": "View", "mydspace.view-btn": "Anzeige", + + // "nav.browse.header": "All of DSpace", "nav.browse.header": "Das gesamte Repositorium", + // "nav.community-browse.header": "By Community", "nav.community-browse.header": "Nach Bereich", + // "nav.language": "Language switch", "nav.language": "Sprachumschalter", + // "nav.login": "Log In", "nav.login": "Einloggen", + // "nav.logout": "Log Out", "nav.logout": "Ausloggen", + // "nav.mydspace": "MyDSpace", "nav.mydspace": "Mein DSpace", + // "nav.search": "Search", "nav.search": "Suche", + // "nav.statistics.header": "Statistics", "nav.statistics.header": "Statistiken", + + // "orgunit.listelement.badge": "Organizational Unit", "orgunit.listelement.badge": "Organisationseinheit", + // "orgunit.page.city": "City", "orgunit.page.city": "Stadt", + // "orgunit.page.country": "Country", "orgunit.page.country": "Land", + // "orgunit.page.dateestablished": "Date established", "orgunit.page.dateestablished": "Ursprungsdatum", + // "orgunit.page.description": "Description", "orgunit.page.description": "Beschreibung", + // "orgunit.page.id": "ID", "orgunit.page.id": "ID", + // "orgunit.page.titleprefix": "Organizational Unit: ", "orgunit.page.titleprefix": "Organisationseinheit: ", + + // "pagination.results-per-page": "Results Per Page", "pagination.results-per-page": "Anzeige pro Seite", + // "pagination.showing.detail": "{{ range }} of {{ total }}", "pagination.showing.detail": "{{ range }} von {{ total }}", + // "pagination.showing.label": "Now showing ", "pagination.showing.label": "Gerade angezeigt ", + // "pagination.sort-direction": "Sort Options", "pagination.sort-direction": "Sortieroptionen", + + // "person.listelement.badge": "Person", "person.listelement.badge": "Person", + // "person.page.birthdate": "Birth Date", "person.page.birthdate": "Geburtsdatum", + // "person.page.email": "Email Address", "person.page.email": "E-Mail-Adresse", + // "person.page.firstname": "First Name", "person.page.firstname": "Vorname", + // "person.page.jobtitle": "Job Title", "person.page.jobtitle": "Berufsbeschreibung", + // "person.page.lastname": "Last Name", "person.page.lastname": "Nachname", + // "person.page.link.full": "Show all metadata", "person.page.link.full": "Alle Metadaten anzeigen", + // "person.page.orcid": "ORCID", "person.page.orcid": "ORCID", + // "person.page.staffid": "Staff ID", "person.page.staffid": "Personalnummer", + // "person.page.titleprefix": "Person: ", "person.page.titleprefix": "Person: ", + // "person.search.results.head": "Person Search Results", "person.search.results.head": "Ergebnisse der Personensuche", + // "person.search.title": "DSpace Angular :: Person Search", "person.search.title": "DSpace Angular :: Personensuche", + + // "project.listelement.badge": "Research Project", "project.listelement.badge": "Forschungsprojekt", + // "project.page.contributor": "Contributors", "project.page.contributor": "Beteiligte", + // "project.page.description": "Description", "project.page.description": "Beschreibung", + // "project.page.expectedcompletion": "Expected Completion", "project.page.expectedcompletion": "Erwartetes Abschlussdatum", + // "project.page.funder": "Funders", "project.page.funder": "Förderer", + // "project.page.id": "ID", "project.page.id": "ID", + // "project.page.keyword": "Keywords", "project.page.keyword": "Schlagwörter", + // "project.page.status": "Status", "project.page.status": "Status", + // "project.page.titleprefix": "Research Project: ", "project.page.titleprefix": "Forschungsvorhaben: ", + // "project.search.results.head": "Project Search Results", + "project.search.results.head": "Suchergebnisse für Projekte", + + + // "publication.listelement.badge": "Publication", "publication.listelement.badge": "Veröffentlichung", + // "publication.page.description": "Description", "publication.page.description": "Beschreibung", + // "publication.page.journal-issn": "Journal ISSN", "publication.page.journal-issn": "Zeitschrift ISSN", + // "publication.page.journal-title": "Journal Title", "publication.page.journal-title": "Zeitschriftentitel", + // "publication.page.publisher": "Publisher", "publication.page.publisher": "Verlag", + // "publication.page.titleprefix": "Publication: ", "publication.page.titleprefix": "Publikation: ", + // "publication.page.volume-title": "Volume Title", "publication.page.volume-title": "Bandtitel", + // "publication.search.results.head": "Publication Search Results", "publication.search.results.head": "Suchergebnisse Publikationen", + // "publication.search.title": "DSpace Angular :: Publication Search", "publication.search.title": "DSpace Angular :: Publikationssuche", + + // "relationships.isAuthorOf": "Authors", "relationships.isAuthorOf": "AutorInnen", + // "relationships.isIssueOf": "Journal Issues", "relationships.isIssueOf": "Zeitschriftenausgaben", + // "relationships.isJournalIssueOf": "Journal Issue", "relationships.isJournalIssueOf": "Zeitschriftenausgabe", + // "relationships.isJournalOf": "Journals", "relationships.isJournalOf": "Zeitschriften", + // "relationships.isOrgUnitOf": "Organizational Units", "relationships.isOrgUnitOf": "Organisationseinheiten", + // "relationships.isPersonOf": "Authors", "relationships.isPersonOf": "AutorInnen", + // "relationships.isProjectOf": "Research Projects", "relationships.isProjectOf": "Forschungsvorhaben", + // "relationships.isPublicationOf": "Publications", "relationships.isPublicationOf": "Publikationen", + // "relationships.isPublicationOfJournalIssue": "Articles", "relationships.isPublicationOfJournalIssue": "Artikel", + // "relationships.isSingleJournalOf": "Journal", "relationships.isSingleJournalOf": "Zeitschrift", + // "relationships.isSingleVolumeOf": "Journal Volume", "relationships.isSingleVolumeOf": "Zeitschriftenband", + // "relationships.isVolumeOf": "Journal Volumes", "relationships.isVolumeOf": "Zeitschriftenbände", + // "relationships.isContributorOf": "Contributors", + "relationships.isContributorOf": "Beteiligte", + + + // "search.description": "", "search.description": "", + // "search.switch-configuration.title": "Show", "search.switch-configuration.title": "Zeige", + // "search.title": "DSpace Angular :: Search", "search.title": "DSpace Angular :: Suche", + // "search.breadcrumbs": "Search", + "search.breadcrumbs": "Suche", + + // "search.filters.applied.f.author": "Author", "search.filters.applied.f.author": "AutorIn", + // "search.filters.applied.f.dateIssued.max": "End date", "search.filters.applied.f.dateIssued.max": "Enddatum", + // "search.filters.applied.f.dateIssued.min": "Start date", "search.filters.applied.f.dateIssued.min": "Anfangsdatum", + // "search.filters.applied.f.dateSubmitted": "Date submitted", "search.filters.applied.f.dateSubmitted": "Datum der Einreichung", + // "search.filters.applied.f.entityType": "Item Type", "search.filters.applied.f.entityType": "Art der Ressource", + // "search.filters.applied.f.has_content_in_original_bundle": "Has files", "search.filters.applied.f.has_content_in_original_bundle": "Hat Dateien", + // "search.filters.applied.f.itemtype": "Type", "search.filters.applied.f.itemtype": "Typ", + // "search.filters.applied.f.namedresourcetype": "Status", "search.filters.applied.f.namedresourcetype": "Status", + // "search.filters.applied.f.subject": "Subject", "search.filters.applied.f.subject": "Thema", + // "search.filters.applied.f.submitter": "Submitter", "search.filters.applied.f.submitter": "Einreichende(r)", + // "search.filters.applied.f.jobTitle": "Job Title", + "search.filters.applied.f.jobTitle": "Berufsbezeichnung", + // "search.filters.applied.f.birthDate.max": "End birth date", + "search.filters.applied.f.birthDate.max": "Ende Geburtsdatum", + // "search.filters.applied.f.birthDate.min": "Start birth date", + "search.filters.applied.f.birthDate.min": "Anfang Geburtsdatum", + + // "search.filters.filter.author.head": "Author", "search.filters.filter.author.head": "AutorIn", + // "search.filters.filter.author.placeholder": "Author name", "search.filters.filter.author.placeholder": "Name des/r AutorIn", + // "search.filters.filter.birthDate.head": "Birth Date", "search.filters.filter.birthDate.head": "Geburtsdatum", + // "search.filters.filter.birthDate.placeholder": "Birth Date", "search.filters.filter.birthDate.placeholder": "Geburtsdatum", + // "search.filters.filter.creativeDatePublished.head": "Date Published", "search.filters.filter.creativeDatePublished.head": "Erscheinungsdatum", + // "search.filters.filter.creativeDatePublished.placeholder": "Date Published", "search.filters.filter.creativeDatePublished.placeholder": "Erscheinungsdatum", + // "search.filters.filter.creativeWorkEditor.head": "Editor", "search.filters.filter.creativeWorkEditor.head": "Herausgeber", + // "search.filters.filter.creativeWorkEditor.placeholder": "Editor", "search.filters.filter.creativeWorkEditor.placeholder": "Herausgeber", + // "search.filters.filter.creativeWorkKeywords.head": "Subject", "search.filters.filter.creativeWorkKeywords.head": "Thema", + // "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", "search.filters.filter.creativeWorkKeywords.placeholder": "Thema", + // "search.filters.filter.creativeWorkPublisher.head": "Publisher", "search.filters.filter.creativeWorkPublisher.head": "Verlag", + // "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", "search.filters.filter.creativeWorkPublisher.placeholder": "Verlag", + // "search.filters.filter.dateIssued.head": "Date", "search.filters.filter.dateIssued.head": "Datum", + // "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", "search.filters.filter.dateIssued.max.placeholder": "Ältestes Datum", + // "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", "search.filters.filter.dateIssued.min.placeholder": "Jüngstes Datum", + // "search.filters.filter.dateSubmitted.head": "Date submitted", "search.filters.filter.dateSubmitted.head": "Datum der Einreichung", + // "search.filters.filter.dateSubmitted.placeholder": "Date submitted", "search.filters.filter.dateSubmitted.placeholder": "Datum der Einreichung", + // "search.filters.filter.entityType.head": "Item Type", "search.filters.filter.entityType.head": "Art der Ressource", + // "search.filters.filter.entityType.placeholder": "Item Type", - "search.filters.filter.entityType.placeholder": "Art der Ressoruce", + "search.filters.filter.entityType.placeholder": "Art der Ressource", + // "search.filters.filter.has_content_in_original_bundle.head": "Has files", "search.filters.filter.has_content_in_original_bundle.head": "Enthält Dateien", + // "search.filters.filter.itemtype.head": "Type", "search.filters.filter.itemtype.head": "Typ", + // "search.filters.filter.itemtype.placeholder": "Type", "search.filters.filter.itemtype.placeholder": "Typ", + // "search.filters.filter.jobTitle.head": "Job Title", "search.filters.filter.jobTitle.head": "Berufsbezeichnung", + // "search.filters.filter.jobTitle.placeholder": "Job Title", "search.filters.filter.jobTitle.placeholder": "Berufsbezeichnung", + // "search.filters.filter.knowsLanguage.head": "Known language", "search.filters.filter.knowsLanguage.head": "Bekannte Sprache", + // "search.filters.filter.knowsLanguage.placeholder": "Known language", "search.filters.filter.knowsLanguage.placeholder": "Bekannte Sprache", + // "search.filters.filter.namedresourcetype.head": "Status", "search.filters.filter.namedresourcetype.head": "Status", + // "search.filters.filter.namedresourcetype.placeholder": "Status", "search.filters.filter.namedresourcetype.placeholder": "Status", + // "search.filters.filter.objectpeople.head": "People", "search.filters.filter.objectpeople.head": "Personen", + // "search.filters.filter.objectpeople.placeholder": "People", "search.filters.filter.objectpeople.placeholder": "Personen", + // "search.filters.filter.organizationAddressCountry.head": "Country", "search.filters.filter.organizationAddressCountry.head": "Land", + // "search.filters.filter.organizationAddressCountry.placeholder": "Country", "search.filters.filter.organizationAddressCountry.placeholder": "Land", + // "search.filters.filter.organizationAddressLocality.head": "City", "search.filters.filter.organizationAddressLocality.head": "Stadt", + // "search.filters.filter.organizationAddressLocality.placeholder": "City", "search.filters.filter.organizationAddressLocality.placeholder": "Stadt", + // "search.filters.filter.organizationFoundingDate.head": "Date Founded", "search.filters.filter.organizationFoundingDate.head": "Gründungsdatum", + // "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", "search.filters.filter.organizationFoundingDate.placeholder": "Gründungsdatum", + // "search.filters.filter.scope.head": "Scope", "search.filters.filter.scope.head": "Bereich", + // "search.filters.filter.scope.placeholder": "Scope filter", "search.filters.filter.scope.placeholder": "Bereichsfilter", + // "search.filters.filter.show-less": "Collapse", "search.filters.filter.show-less": "Zusammenklappen", + // "search.filters.filter.show-more": "Show more", "search.filters.filter.show-more": "Mehr anzeigen", + // "search.filters.filter.subject.head": "Subject", "search.filters.filter.subject.head": "Thema", + // "search.filters.filter.subject.placeholder": "Subject", "search.filters.filter.subject.placeholder": "Thema", + // "search.filters.filter.submitter.head": "Submitter", "search.filters.filter.submitter.head": "Einreichende(r)", + // "search.filters.filter.submitter.placeholder": "Submitter", "search.filters.filter.submitter.placeholder": "Einreichende(r)", + + // "search.filters.head": "Filters", "search.filters.head": "Filter", + // "search.filters.reset": "Reset filters", "search.filters.reset": "Filter zurücksetzen", + + // "search.form.search": "Search", "search.form.search": "Suche", + // "search.form.search_dspace": "Search DSpace", "search.form.search_dspace": "Suche in DSpace", + // "search.form.search_mydspace": "Search MyDSpace", "search.form.search_mydspace": "Suche im persönlichen Arbeitsbereich", + + // "search.results.head": "Search Results", "search.results.head": "Suchergebnisse", + // "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", "search.results.no-results": "Ihre Suche führte zu keinem Ergebnis. Versuchen Sie es mit ", + // "search.results.no-results-link": "quotes around it", "search.results.no-results-link": "Anführungszeichen", + // "search.results.empty": "Your search returned no results.", + "search.results.empty": "Ihre Suche führte zu keinem Ergebnis.", + + + // "search.sidebar.close": "Back to results", "search.sidebar.close": "Zurück zu den Ergebnissen", + // "search.sidebar.filters.title": "Filters", "search.sidebar.filters.title": "Filter", + // "search.sidebar.open": "Search Tools", "search.sidebar.open": "Suchwerkzeuge", + // "search.sidebar.results": "results", "search.sidebar.results": "Ergebnisse", + // "search.sidebar.settings.rpp": "Results per page", "search.sidebar.settings.rpp": "Ergebnisse pro Seite", + // "search.sidebar.settings.sort-by": "Sort By", "search.sidebar.settings.sort-by": "Sortieren nach", + // "search.sidebar.settings.title": "Settings", "search.sidebar.settings.title": "Einstellungen", + + // "search.view-switch.show-detail": "Show detail", "search.view-switch.show-detail": "Detailanzeige", + // "search.view-switch.show-grid": "Show as grid", "search.view-switch.show-grid": "Anzeige als Raster", + // "search.view-switch.show-list": "Show as list", "search.view-switch.show-list": "Anzeige als Liste", + + // "sorting.dc.title.ASC": "Title Ascending", "sorting.dc.title.ASC": "Titel aufsteigend", + // "sorting.dc.title.DESC": "Title Descending", "sorting.dc.title.DESC": "Titel absteigend", + // "sorting.score.DESC": "Relevance", "sorting.score.DESC": "Relevanz", + + // "submission.edit.title": "Edit Submission", "submission.edit.title": "Einreichung bearbeiten", + // "submission.general.cannot_submit": "You have not the privilege to make a new submission.", "submission.general.cannot_submit": "Sie verfügen nicht über die Rechte, eine neue Einreichung zu machen.", + // "submission.general.deposit": "Deposit", "submission.general.deposit": "Einreichen", + // "submission.general.discard.confirm.cancel": "Cancel", "submission.general.discard.confirm.cancel": "Abbrechen", + // "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", "submission.general.discard.confirm.info": "Diese Aktion kann nicht rückgängig gemacht werden. Sind Sie sicher?", + // "submission.general.discard.confirm.submit": "Yes, I'm sure", "submission.general.discard.confirm.submit": "Ja, ich bin sicher", + // "submission.general.discard.confirm.title": "Discard submission", "submission.general.discard.confirm.title": "Einreichung verwerfen", + // "submission.general.discard.submit": "Discard", "submission.general.discard.submit": "Verwerfen", + // "submission.general.save": "Save", "submission.general.save": "Speichern", + // "submission.general.save-later": "Save for later", "submission.general.save-later": "Für später speichern", + + + // "submission.sections.describe.relationship-lookup.close": "Close", + "submission.sections.describe.relationship-lookup.close": "Schließen", + + // "submission.sections.describe.relationship-lookup.external-source.added": "Successfully added local entry to the selection", + "submission.sections.describe.relationship-lookup.external-source.added": "Der lokale Eintrage wurde erfolgreich zur Auswahl hinzugefügt.", + + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Author": "Import remote author", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Author": "AutorIn importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal": "Import remote journal", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal": "Zeitschrift importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Issue": "Import remote journal issue", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Issue": "Zeitschriftenausgabe importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Volume": "Import remote journal volume", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Volume": "Zeitschriftenband importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.title": "Import Remote Author", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.title": "AutorIn importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.local-entity": "Successfully added local author to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.local-entity": "Lokale/r Autor/in der Auswahl erfolgreich hinzugefügt", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.new-entity": "Successfully imported and added external author to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.new-entity": "Externe/r Autor/in erfolgreich zur Auswahl hinzugefügt", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Authority", + "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Referenz", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.authority.new": "Import as a new local authority entry", + "submission.sections.describe.relationship-lookup.external-source.import-modal.authority.new": " Als neuen lokalen Referenzwert importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Cancel", + "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Abbrechen", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.collection": "Select a collection to import new entries to", + "submission.sections.describe.relationship-lookup.external-source.import-modal.collection": "Wählen Sie eine Sammlung, um neue Einträge dorthin zu importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Entities", + "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Entitäten", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.entities.new": "Import as a new local entity", + "submission.sections.describe.relationship-lookup.external-source.import-modal.entities.new": "Als neue lokale Entität importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name", + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Von LC Name importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID", + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Von ORCID importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal", + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Von Sherpa Zeitschriften importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaPublisher": "Importing from Sherpa Publisher", + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaPublisher": "Von Sherpa Verlag importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Import", + "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Import", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.title": "Import Remote Journal", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.title": "Zeitschrift von externer Quelle importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.local-entity": "Successfully added local journal to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.local-entity": "Lokale Zeitschrift wurde erfolgreich zur Auswahl hinzugefügt", + + // "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": "Externe Zeitschrift wurde erfolgreich importiert und der Auswahl hinzugefügt", + + // "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": "Externe Zeitschriftenausgabe importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.local-entity": "Successfully added local journal issue to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.local-entity": "Lokale Zeitschriftenausgabe wurde erfolgreich zur Auswahl hinzugefügt", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.new-entity": "Successfully imported and added external journal issue to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.new-entity": "Externe Zeitschriftenausgabe wurde erfolgreich importiert und zur Auswahl hinzugefügt", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.title": "Import Remote Journal Volume", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.title": "Externen Zeitschriftenband importieren", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.local-entity": "Successfully added local journal volume to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.local-entity": "Der lokale Zeitschriftenband wurde erfolgreich zur Auswahl hinzugefügt", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.new-entity": "Successfully imported and added external journal volume to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.new-entity": "Externer Zeitschriftenband wurde erfolgreich importiert und zur Auswahl hinzugefügt", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Select a local match:", + "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Wählen Sie einen lokalen Treffer aus:", + + // "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all", + "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Alle abwählen", + + // "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page", + "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Seite abwählen", + + // "submission.sections.describe.relationship-lookup.search-tab.loading": "Loading...", + "submission.sections.describe.relationship-lookup.search-tab.loading": "Laden...", + + // "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Search query", + "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Suchanfrage", + + // "submission.sections.describe.relationship-lookup.search-tab.search": "Go", + "submission.sections.describe.relationship-lookup.search-tab.search": "Los", + + // "submission.sections.describe.relationship-lookup.search-tab.select-all": "Select all", + "submission.sections.describe.relationship-lookup.search-tab.select-all": "Alles auswählen", + + // "submission.sections.describe.relationship-lookup.search-tab.select-page": "Select page", + "submission.sections.describe.relationship-lookup.search-tab.select-page": "Seite auswählen", + + // "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", + "submission.sections.describe.relationship-lookup.selected": "{{ size }} Ressourcen ausgewählt", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Lokale AutorInnen ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Lokale Zeitschriften ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Local Journal Issues ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Lokale Zeitschriftenausgaben ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Local Journal Volumes ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Lokale Zeitschriftenbände ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Zeitschriften ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Verlage ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Namen ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Suche nach Fördereinrichtung", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding": "Search for Funding", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding": "Suche nach Förderung", + + // "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", + "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Aktuelle Auswahl ({{ count }})", + + // "submission.sections.describe.relationship-lookup.title.Journal Issue": "Journal Issues", + "submission.sections.describe.relationship-lookup.title.Journal Issue": "Zeitschriftenausgaben", + + // "submission.sections.describe.relationship-lookup.title.Journal Volume": "Journal Volumes", + "submission.sections.describe.relationship-lookup.title.Journal Volume": "Zeitschriftenbände", + + // "submission.sections.describe.relationship-lookup.title.Journal": "Journals", + "submission.sections.describe.relationship-lookup.title.Journal": "Zeitschriften", + + // "submission.sections.describe.relationship-lookup.title.Author": "Authors", + "submission.sections.describe.relationship-lookup.title.Author": "AutorInnen", + + // "submission.sections.describe.relationship-lookup.title.Funding Agency": "Funding Agency", + "submission.sections.describe.relationship-lookup.title.Funding Agency": "Fördereinrichtung", + + // "submission.sections.describe.relationship-lookup.title.Funding": "Funding", + "submission.sections.describe.relationship-lookup.title.Funding": "Förderung", + + // "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown", + "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Liste umschalten", + + // "submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings", + "submission.sections.describe.relationship-lookup.selection-tab.settings": "Einstellungen", + + // "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Your selection is currently empty.", + "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Ihre Auswahl ist momentan leer.", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.Author": "Selected Authors", + "submission.sections.describe.relationship-lookup.selection-tab.title.Author": "Augewählte AutorInnen", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals", + "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Ausgewählte Zeitschriften", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Volume": "Selected Journal Volume", + "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Volume": "Ausgewählte Zeitschriftenbände", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Ausgewählte Zeitschriftenausgaben", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Suchergebnisse", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Suchergebnisse", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Suchergebnisse", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Suchergebnisse", + + // "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.", + "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Möchten Sie \"{{ value }}\" als Namesvariante für diese Person speichern? So können Sie und andere diese bei zukünftigen Veröffentlichungen verwenden. Wenn Sie das nicht tun, können Sie die Variante immer noch für diese Veröffentlichung verwenden.", + + // "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant", + "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Als Namensvariante speichern", + + // "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Use only for this submission", + "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Nur für diese Einreichung verwenden", + // "submission.sections.general.add-more": "Add more", "submission.sections.general.add-more": "Mehr Hinzufügen", + // "submission.sections.general.collection": "Collection", "submission.sections.general.collection": "Sammlung", + // "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", "submission.sections.general.deposit_error_notice": "Beim Einreichen der Ressoruce ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.", + // "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", "submission.sections.general.deposit_success_notice": "Veröffentlichung erfolgreich eingereicht", + // "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", "submission.sections.general.discard_error_notice": "Beim Verwerfen der Einreichung ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal", + // "submission.sections.general.discard_success_notice": "Submission discarded successfully.", "submission.sections.general.discard_success_notice": "Einreichung erfolgreich verworfen.", + // "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", "submission.sections.general.metadata-extracted": "Neue Metainformation wurden extrahier unt dem Bereich {{sectionId}} zugeordnet.", + // "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", "submission.sections.general.metadata-extracted-new-section": "Neuer Bereich {{sectionId}} wurde zur Einreichung hinzugefügt.", + // "submission.sections.general.no-collection": "No collection found", "submission.sections.general.no-collection": "Keine Sammlung gefunden", + // "submission.sections.general.no-sections": "No options available", "submission.sections.general.no-sections": "Es stehen keine Optionen zur Verfügung", + // "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", "submission.sections.general.save_error_notice": "Beim Speichern der Ressource ist ein Fehler aufgetreten, bitte versuchen Sie es später noch einmal.", + // "submission.sections.general.save_success_notice": "Submission saved successfully.", "submission.sections.general.save_success_notice": "Einreichung erfolgreich gespeichert.", + // "submission.sections.general.search-collection": "Search for a collection", "submission.sections.general.search-collection": "Suche nach einer Sammlung", + // "submission.sections.general.sections_not_valid": "There are incomplete sections.", "submission.sections.general.sections_not_valid": "Es gibt unvollständige Abschnitte.", + + // "submission.sections.submit.progressbar.cclicense": "Creative commons license", "submission.sections.submit.progressbar.cclicense": "Creative Commons Lizenz", + // "submission.sections.submit.progressbar.describe.recycle": "Recycle", "submission.sections.submit.progressbar.describe.recycle": "Wiederverwerten", + // "submission.sections.submit.progressbar.describe.stepcustom": "Describe", "submission.sections.submit.progressbar.describe.stepcustom": "Beschreiben", + // "submission.sections.submit.progressbar.describe.stepone": "Describe", "submission.sections.submit.progressbar.describe.stepone": "Beschreiben", + // "submission.sections.submit.progressbar.describe.steptwo": "Describe", "submission.sections.submit.progressbar.describe.steptwo": "Beschreiben", + // "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", "submission.sections.submit.progressbar.detect-duplicate": "Mögliche Dubletten", + // "submission.sections.submit.progressbar.license": "Deposit license", "submission.sections.submit.progressbar.license": "Einreichlizenz", + // "submission.sections.submit.progressbar.upload": "Upload files", "submission.sections.submit.progressbar.upload": "Hochgeladene Dateien", + + // "submission.sections.upload.delete.confirm.cancel": "Cancel", "submission.sections.upload.delete.confirm.cancel": "Abbrechen", + // "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", "submission.sections.upload.delete.confirm.info": "Diese Aktion kann nicht rückgängig gemacht werden. Sind Sie sicher?", + // "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", "submission.sections.upload.delete.confirm.submit": "Ja, ich bin sicher", + // "submission.sections.upload.delete.confirm.title": "Delete bitstream", "submission.sections.upload.delete.confirm.title": "Datei löschen", + // "submission.sections.upload.delete.submit": "Delete", "submission.sections.upload.delete.submit": "Löschen", + // "submission.sections.upload.drop-message": "Drop files to attach them to the item", "submission.sections.upload.drop-message": "Dateien herüberziehen, um sie der Ressource hinzuzufügen", + // "submission.sections.upload.form.access-condition-label": "Access condition type", "submission.sections.upload.form.access-condition-label": "Zugriffsbedingung Typ", + // "submission.sections.upload.form.date-required": "Date is required.", "submission.sections.upload.form.date-required": "Datum erforderlich.", + // "submission.sections.upload.form.from-label": "Access grant from", "submission.sections.upload.form.from-label": "Zugriff gewährt ab", + // "submission.sections.upload.form.from-placeholder": "From", "submission.sections.upload.form.from-placeholder": "Ab", + // "submission.sections.upload.form.group-label": "Group", "submission.sections.upload.form.group-label": "Gruppe", + // "submission.sections.upload.form.group-required": "Group is required.", "submission.sections.upload.form.group-required": "Gruppe ist erforderlich", + // "submission.sections.upload.form.until-label": "Access grant until", "submission.sections.upload.form.until-label": "Zugriff gewährt bis", + // "submission.sections.upload.form.until-placeholder": "Until", "submission.sections.upload.form.until-placeholder": "Bis", + // "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", "submission.sections.upload.header.policy.default.nolist": "In diese Sammlung {{collectionName}} hochgeladene Dateien werden für folgende(n) Gruppe(n) zugänglich sein:", + // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", "submission.sections.upload.header.policy.default.withlist": "Bitte beachten Sie, dass in diese Sammlung {{collectionName}} hochgeladene Dateien zugüglich zu dem, was für einzelne Dateien entschieden wurde, für folgende Gruppe(n) zugänglich sein:", + // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", "submission.sections.upload.info": "Hier finden Sie alle Dateien, die aktuell zur Ressource gehören. Sie können die Metadaten und Zugriffsrechte bearbeiten oder weitere Dateien hinzufügen, indem Sie sie einfach irgenwo auf diese Seite ziehen.", + // "submission.sections.upload.no-entry": "No", "submission.sections.upload.no-entry": "Kein Eintrag", + // "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", "submission.sections.upload.no-file-uploaded": "Es wurde noch keine Datei hochgeladen", + // "submission.sections.upload.save-metadata": "Save metadata", "submission.sections.upload.save-metadata": "Metadaten speichern", + // "submission.sections.upload.undo": "Cancel", "submission.sections.upload.undo": "Abbrechen", + // "submission.sections.upload.upload-failed": "Upload failed", "submission.sections.upload.upload-failed": "Hochladen fehlgeschlagen", + // "submission.sections.upload.upload-successful": "Upload successful", "submission.sections.upload.upload-successful": "Hochladen erfolgreich", + + // "submission.submit.title": "Submission", "submission.submit.title": "Einreichung", + + // "submission.workflow.generic.delete": "Delete", "submission.workflow.generic.delete": "Löschen", + // "submission.workflow.generic.delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.", "submission.workflow.generic.delete-help": "Wenn Sie die Ressource verwerfen möchten, Wählen Sie \"Löschen\". Sie werden dies noch einmal gefragt, um die Aktion zu bestätigen.", + // "submission.workflow.generic.edit": "Edit", "submission.workflow.generic.edit": "Bearbeiten", + // "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", "submission.workflow.generic.edit-help": "Wählen Sie diese Option, um die Metadaten der Ressource zu bearbeiten.", + // "submission.workflow.generic.view": "View", "submission.workflow.generic.view": "Anzeige", + // "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", "submission.workflow.generic.view-help": "Wählen Sie diese Option, um die Metadaten der Ressourcen anzuzeigen", + + // "submission.workflow.tasks.claimed.approve": "Approve", "submission.workflow.tasks.claimed.approve": "Zustimmen", + // "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", "submission.workflow.tasks.claimed.approve_help": "Wenn Sie die Ressource begutachtet und die Aufnahme in die Sammlung befürworten, wählen Sie \"Zustimmen\".", + // "submission.workflow.tasks.claimed.edit": "Edit", "submission.workflow.tasks.claimed.edit": "Bearbeiten", + // "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", "submission.workflow.tasks.claimed.edit_help": "Wählen Sie diese Option, um die Metadaten der Ressource zu bearbeiten.", + // "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", "submission.workflow.tasks.claimed.reject.reason.info": "Bitte geben Sie den Grund für die Ablehnung der eingereichten Ressource in das Feld unten ein. Bitte geben Sie an ob und wie der/die Einreichenden das Problem beheben und die Ressource erneut einreichen kann.", + // "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", "submission.workflow.tasks.claimed.reject.reason.placeholder": "Beschreiben Sie den Grund für die Ablehnung", + // "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", "submission.workflow.tasks.claimed.reject.reason.submit": "Ressource Ablehnen", + // "submission.workflow.tasks.claimed.reject.reason.title": "Reason", "submission.workflow.tasks.claimed.reject.reason.title": "Grund", + // "submission.workflow.tasks.claimed.reject.submit": "Reject", "submission.workflow.tasks.claimed.reject.submit": "Ablehnen", + // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", "submission.workflow.tasks.claimed.reject_help": "Wenn Sie die Ressource begutachte und als ungeeignet für die Aufnahme in die Sammlung befunden haben, wählen Sie \"Ablehnen\". Sie haben dann die Möglichkeit dem/der Einreichenden, den Grund für die Ablehnung zu erklären und ob es eine Möglichkeit gibt, durch entsprechenden Änderungen die Ressource erneut einzureichen.", + // "submission.workflow.tasks.claimed.return": "Return to pool", "submission.workflow.tasks.claimed.return": "Zurück in den gemeinsamen Aufgabenbereich", + // "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", "submission.workflow.tasks.claimed.return_help": "Aufgabe in den gemeinsamen Aufgabenbereich überführen, so dass ein anderer Bearbeiter die Aufgabe übernehmen kann.", + + // "submission.workflow.tasks.generic.error": "Error occurred during operation...", "submission.workflow.tasks.generic.error": "Ein Fehler ist aufgetreten...", + // "submission.workflow.tasks.generic.processing": "Processing...", "submission.workflow.tasks.generic.processing": "Verarbeitung läuft...", + // "submission.workflow.tasks.generic.submitter": "Submitter", "submission.workflow.tasks.generic.submitter": "Einreichende(r)", + // "submission.workflow.tasks.generic.success": "Operation successful", "submission.workflow.tasks.generic.success": "Aktion erfolgreich", + + // "submission.workflow.tasks.pool.claim": "Claim", "submission.workflow.tasks.pool.claim": "Übernehmen", + // "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", "submission.workflow.tasks.pool.claim_help": "Aufgabe übernehmen.", + // "submission.workflow.tasks.pool.hide-detail": "Hide detail", "submission.workflow.tasks.pool.hide-detail": "Details verbergen", + // "submission.workflow.tasks.pool.show-detail": "Show detail", - "submission.workflow.tasks.pool.show-detail": "Details anzeigen", + "submission.workflow.tasks.pool.show-detail": "Details anzeigen", + + // "title": "DSpace", "title": "DSpace", + + // "uploader.browse": "browse", "uploader.browse": "stöbern", + // "uploader.drag-message": "Drag & Drop your files here", "uploader.drag-message": "Ziehen Sie Ihre Dateien hierhin", + // "uploader.or": ", or", "uploader.or": " oder", + // "uploader.processing": "Processing", "uploader.processing": "Bearbeitung läuft", + // "uploader.queue-length": "Queue length", "uploader.queue-length": "Länge der Warteschleife", + + // "virtual-metadata.delete-item.info": "Select the types for which you want to save the virtual metadata as real metadata", + "virtual-metadata.delete-item.info": "Wählen Sie die Typen für die Sie die virtuellen Metadaten als reelle Metadaten speichern wollen", + + // "virtual-metadata.delete-item.modal-head": "The virtual metadata of this relation", + "virtual-metadata.delete-item.modal-head": "Virtuelle Metadaten dieser Relation", + + // "virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata", + "virtual-metadata.delete-relationship.modal-head": "Wählen Sie die Ressourcen für die Sie die virtuellen Metadaten als reelle Metadaten speichern wollen", + + + } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index d189fe72a3..f0c8c81d43 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -176,12 +176,12 @@ "admin.access-control.epeople.search.head": "Search", - "admin.access-control.epeople.search.scope.name": "Name", - - "admin.access-control.epeople.search.scope.email": "E-mail (exact)", + "admin.access-control.epeople.button.see-all": "Browse All", "admin.access-control.epeople.search.scope.metadata": "Metadata", + "admin.access-control.epeople.search.scope.email": "E-mail (exact)", + "admin.access-control.epeople.search.button": "Search", "admin.access-control.epeople.button.add": "Add EPerson", @@ -190,13 +190,13 @@ "admin.access-control.epeople.table.name": "Name", - "admin.access-control.epeople.table.email": "E-mail", + "admin.access-control.epeople.table.email": "E-mail (exact)", "admin.access-control.epeople.table.edit": "Edit", - "item.access-control.epeople.table.edit.buttons.edit": "Edit", + "admin.access-control.epeople.table.edit.buttons.edit": "Edit \"{{name}}\"", - "item.access-control.epeople.table.edit.buttons.remove": "Remove", + "admin.access-control.epeople.table.edit.buttons.remove": "Delete \"{{name}}\"", "admin.access-control.epeople.no-items": "No EPeople to show.", @@ -228,12 +228,149 @@ "admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"", + "admin.access-control.epeople.form.groupsEPersonIsMemberOf": "Member of these groups:", + + "admin.access-control.epeople.form.table.id": "ID", + + "admin.access-control.epeople.form.table.name": "Name", + + "admin.access-control.epeople.form.memberOfNoGroups": "This EPerson is not a member of any groups", + + "admin.access-control.epeople.form.goToGroups": "Add to groups", + "admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"", "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"", + "admin.access-control.groups.title": "DSpace Angular :: Groups", + + "admin.access-control.groups.head": "Groups", + + "admin.access-control.groups.button.add": "Add group", + + "admin.access-control.groups.search.head": "Search groups", + + "admin.access-control.groups.button.see-all": "Browse all", + + "admin.access-control.groups.search.button": "Search", + + "admin.access-control.groups.table.id": "ID", + + "admin.access-control.groups.table.name": "Name", + + "admin.access-control.groups.table.members": "Members", + + "admin.access-control.groups.table.comcol": "Community / Collection", + + "admin.access-control.groups.table.edit": "Edit", + + "admin.access-control.groups.table.edit.buttons.edit": "Edit \"{{name}}\"", + + "admin.access-control.groups.table.edit.buttons.remove": "Delete \"{{name}}\"", + + "admin.access-control.groups.no-items": "No groups found with this in their name or this as UUID", + + "admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"", + + "admin.access-control.groups.notification.deleted.failure": "Failed to delete group \"{{name}}\"", + + + "admin.access-control.groups.form.head.create": "Create group", + + "admin.access-control.groups.form.head.edit": "Edit group", + + "admin.access-control.groups.form.groupName": "Group name", + + "admin.access-control.groups.form.groupDescription": "Description", + + "admin.access-control.groups.form.notification.created.success": "Successfully created Group \"{{name}}\"", + + "admin.access-control.groups.form.notification.created.failure": "Failed to create Group \"{{name}}\"", + + "admin.access-control.groups.form.notification.created.failure.groupNameInUse": "Failed to create Group with name: \"{{name}}\", make sure the name is not already in use.", + + "admin.access-control.groups.form.members-list.head": "EPeople", + + "admin.access-control.groups.form.members-list.search.head": "Add EPeople", + + "admin.access-control.groups.form.members-list.button.see-all": "Browse All", + + "admin.access-control.groups.form.members-list.headMembers": "Current Members", + + "admin.access-control.groups.form.members-list.search.scope.metadata": "Metadata", + + "admin.access-control.groups.form.members-list.search.scope.email": "E-mail (exact)", + + "admin.access-control.groups.form.members-list.search.button": "Search", + + "admin.access-control.groups.form.members-list.table.id": "ID", + + "admin.access-control.groups.form.members-list.table.name": "Name", + + "admin.access-control.groups.form.members-list.table.edit": "Remove / Add", + + "admin.access-control.groups.form.members-list.table.edit.buttons.remove": "Remove member with name \"{{name}}\"", + + "admin.access-control.groups.form.members-list.notification.success.addMember": "Successfully added member: \"{{name}}\"", + + "admin.access-control.groups.form.members-list.notification.failure.addMember": "Failed to add member: \"{{name}}\"", + + "admin.access-control.groups.form.members-list.notification.success.deleteMember": "Successfully deleted member: \"{{name}}\"", + + "admin.access-control.groups.form.members-list.notification.failure.deleteMember": "Failed to delete member: \"{{name}}\"", + + "admin.access-control.groups.form.members-list.table.edit.buttons.add": "Add member with name \"{{name}}\"", + + "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", + + "admin.access-control.groups.form.members-list.no-members-yet": "No members in group yet, search and add.", + + "admin.access-control.groups.form.members-list.no-items": "No EPeople found in that search", + + "admin.access-control.groups.form.subgroups-list.head": "Groups", + + "admin.access-control.groups.form.subgroups-list.search.head": "Add Subgroup", + + "admin.access-control.groups.form.subgroups-list.button.see-all": "Browse All", + + "admin.access-control.groups.form.subgroups-list.headSubgroups": "Current Subgroups", + + "admin.access-control.groups.form.subgroups-list.search.button": "Search", + + "admin.access-control.groups.form.subgroups-list.table.id": "ID", + + "admin.access-control.groups.form.subgroups-list.table.name": "Name", + + "admin.access-control.groups.form.subgroups-list.table.edit": "Remove / Add", + + "admin.access-control.groups.form.subgroups-list.table.edit.buttons.remove": "Remove subgroup with name \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.table.edit.buttons.add": "Add subgroup with name \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.table.edit.currentGroup": "Current group", + + "admin.access-control.groups.form.subgroups-list.notification.success.addSubgroup": "Successfully added subgroup: \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.notification.failure.addSubgroup": "Failed to add subgroup: \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.notification.success.deleteSubgroup": "Successfully deleted subgroup: \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.notification.failure.deleteSubgroup": "Failed to delete subgroup: \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", + + "admin.access-control.groups.form.subgroups-list.notification.failure.subgroupToAddIsActiveGroup": "This is the current group, can't be added.", + + "admin.access-control.groups.form.subgroups-list.no-items": "No groups found with this in their name or this as UUID", + + "admin.access-control.groups.form.subgroups-list.no-subgroups-yet": "No subgroups in group yet.", + + "admin.access-control.groups.form.return": "Return to groups", + + + "admin.search.breadcrumbs": "Administrative Search", "admin.search.collection.edit": "Edit", @@ -268,6 +405,42 @@ + "bitstream.edit.bitstream": "Bitstream: ", + + "bitstream.edit.form.description.hint": "Optionally, provide a brief description of the file, for example \"Main article\" or \"Experiment data readings\".", + + "bitstream.edit.form.description.label": "Description", + + "bitstream.edit.form.embargo.hint": "The first day from which access is allowed. This date cannot be modified on this form. To set an embargo date for a bitstream, go to the Item Status tab, click Authorizations..., create or edit the bitstream's READ policy, and set the Start Date as desired.", + + "bitstream.edit.form.embargo.label": "Embargo until specific date", + + "bitstream.edit.form.fileName.hint": "Change the filename for the bitstream. Note that this will change the display bitstream URL, but old links will still resolve as long as the sequence ID does not change.", + + "bitstream.edit.form.fileName.label": "Filename", + + "bitstream.edit.form.newFormat.label": "Describe new format", + + "bitstream.edit.form.newFormat.hint": "The application you used to create the file, and the version number (for example, \"ACMESoft SuperApp version 1.5\").", + + "bitstream.edit.form.primaryBitstream.label": "Primary bitstream", + + "bitstream.edit.form.selectedFormat.hint": "If the format is not in the above list, select \"format not in list\" above and describe it under \"Describe new format\".", + + "bitstream.edit.form.selectedFormat.label": "Selected Format", + + "bitstream.edit.form.selectedFormat.unknown": "Format not in list", + + "bitstream.edit.notifications.error.format.title": "An error occurred saving the bitstream's format", + + "bitstream.edit.notifications.saved.content": "Your changes to this bitstream were saved.", + + "bitstream.edit.notifications.saved.title": "Bitstream saved", + + "bitstream.edit.title": "Edit bitstream", + + + "browse.comcol.by.author": "By Author", "browse.comcol.by.dateissued": "By Issue Date", @@ -590,6 +763,60 @@ + "comcol-role.edit.no-group": "None", + + "comcol-role.edit.create": "Create", + + "comcol-role.edit.restrict": "Restrict", + + "comcol-role.edit.delete": "Delete", + + + "comcol-role.edit.community-admin.name": "Administrators", + + "comcol-role.edit.collection-admin.name": "Administrators", + + + "comcol-role.edit.community-admin.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", + + "comcol-role.edit.collection-admin.description": "Collection administrators decide who can submit items to the collection, edit item metadata (after submission), and add (map) existing items from other collections to this collection (subject to authorization for that collection).", + + + "comcol-role.edit.submitters.name": "Submitters", + + "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", + + + "comcol-role.edit.item_read.name": "Default item read access", + + "comcol-role.edit.item_read.description": "E-People and Groups that can read new items submitted to this collection. Changes to this role are not retroactive. Existing items in the system will still be viewable by those who had read access at the time of their addition.", + + "comcol-role.edit.item_read.anonymous-group": "Default read for incoming items is currently set to Anonymous.", + + + "comcol-role.edit.bitstream_read.name": "Default bitstream read access", + + "comcol-role.edit.bitstream_read.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", + + "comcol-role.edit.bitstream_read.anonymous-group": "Default read for incoming bitstreams is currently set to Anonymous.", + + + "comcol-role.edit.editor.name": "Editors", + + "comcol-role.edit.editor.description": "Editors are able to edit the metadata of incoming submissions, and then accept or reject them.", + + + "comcol-role.edit.finaleditor.name": "Final editors", + + "comcol-role.edit.finaleditor.description": "Final editors are able to edit the metadata of incoming submissions, but will not be able to reject them.", + + + "comcol-role.edit.reviewer.name": "Reviewers", + + "comcol-role.edit.reviewer.description": "Reviewers are able to accept or reject incoming submissions. However, they are not able to edit the submission's metadata.", + + + "community.form.abstract": "Short Description", "community.form.description": "Introductory text (HTML)", @@ -638,6 +865,8 @@ + "error.bitstream": "Error fetching bitstream", + "error.browse-by": "Error fetching items", "error.collection": "Error fetching collection", @@ -745,6 +974,93 @@ + + "item.bitstreams.upload.bundle": "Bundle", + + "item.bitstreams.upload.bundle.placeholder": "Select a bundle", + + "item.bitstreams.upload.bundle.new": "Create bundle", + + "item.bitstreams.upload.bundles.empty": "This item doesn\'t contain any bundles to upload a bitstream to.", + + "item.bitstreams.upload.cancel": "Cancel", + + "item.bitstreams.upload.drop-message": "Drop a file to upload", + + "item.bitstreams.upload.item": "Item: ", + + "item.bitstreams.upload.notifications.bundle.created.content": "Successfully created new bundle.", + + "item.bitstreams.upload.notifications.bundle.created.title": "Created bundle", + + "item.bitstreams.upload.notifications.upload.failed": "Upload failed. Please verify the content before retrying.", + + "item.bitstreams.upload.title": "Upload bitstream", + + + + "item.edit.bitstreams.bundle.edit.buttons.upload": "Upload", + + "item.edit.bitstreams.bundle.displaying": "Currently displaying {{ amount }} bitstreams of {{ total }}.", + + "item.edit.bitstreams.bundle.load.all": "Load all ({{ total }})", + + "item.edit.bitstreams.bundle.load.more": "Load more", + + "item.edit.bitstreams.bundle.name": "BUNDLE: {{ name }}", + + "item.edit.bitstreams.discard-button": "Discard", + + "item.edit.bitstreams.edit.buttons.download": "Download", + + "item.edit.bitstreams.edit.buttons.drag": "Drag", + + "item.edit.bitstreams.edit.buttons.edit": "Edit", + + "item.edit.bitstreams.edit.buttons.remove": "Remove", + + "item.edit.bitstreams.edit.buttons.undo": "Undo changes", + + "item.edit.bitstreams.empty": "This item doesn't contain any bitstreams. Click the upload button to create one.", + + "item.edit.bitstreams.headers.actions": "Actions", + + "item.edit.bitstreams.headers.bundle": "Bundle", + + "item.edit.bitstreams.headers.description": "Description", + + "item.edit.bitstreams.headers.format": "Format", + + "item.edit.bitstreams.headers.name": "Name", + + "item.edit.bitstreams.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + + "item.edit.bitstreams.notifications.discarded.title": "Changes discarded", + + "item.edit.bitstreams.notifications.move.failed.title": "Error moving bitstreams", + + "item.edit.bitstreams.notifications.move.saved.content": "Your move changes to this item's bitstreams and bundles have been saved.", + + "item.edit.bitstreams.notifications.move.saved.title": "Move changes saved", + + "item.edit.bitstreams.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", + + "item.edit.bitstreams.notifications.outdated.title": "Changes outdated", + + "item.edit.bitstreams.notifications.remove.failed.title": "Error deleting bitstream", + + "item.edit.bitstreams.notifications.remove.saved.content": "Your removal changes to this item's bitstreams have been saved.", + + "item.edit.bitstreams.notifications.remove.saved.title": "Removal changes saved", + + "item.edit.bitstreams.reinstate-button": "Undo", + + "item.edit.bitstreams.save-button": "Save", + + "item.edit.bitstreams.upload-button": "Upload", + + + "item.edit.delete.cancel": "Cancel", "item.edit.delete.confirm": "Delete", @@ -943,7 +1259,7 @@ - "item.edit.tabs.bitstreams.head": "Item Bitstreams", + "item.edit.tabs.bitstreams.head": "Bitstreams", "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", @@ -951,11 +1267,11 @@ "item.edit.tabs.curate.title": "Item Edit - Curate", - "item.edit.tabs.metadata.head": "Item Metadata", + "item.edit.tabs.metadata.head": "Metadata", "item.edit.tabs.metadata.title": "Item Edit - Metadata", - "item.edit.tabs.relationships.head": "Item Relationships", + "item.edit.tabs.relationships.head": "Relationships", "item.edit.tabs.relationships.title": "Item Edit - Relationships", @@ -993,7 +1309,7 @@ "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", - "item.edit.tabs.status.head": "Item Status", + "item.edit.tabs.status.head": "Status", "item.edit.tabs.status.labels.handle": "Handle", @@ -1162,6 +1478,10 @@ + "loading.bitstream": "Loading bitstream...", + + "loading.bitstreams": "Loading bitstreams...", + "loading.browse-by": "Loading items...", "loading.browse-by-page": "Loading page...", @@ -2255,6 +2575,9 @@ "administrativeView.search.results.head": "Administrative Search", + "menu.section.admin_search": "Admin Search", + + "uploader.browse": "browse", diff --git a/src/assets/i18n/fr.json5 b/src/assets/i18n/fr.json5 index 398c57e6b2..68808a325c 100644 --- a/src/assets/i18n/fr.json5 +++ b/src/assets/i18n/fr.json5 @@ -1,3220 +1,1596 @@ { // "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. ", - // TODO New key - Add a translation - "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": "La page que vous recherchez n'a pas pu être localisée. Cette page pourrait avoir été déplacée ou supprimée. Vous pouvez utiliser le bouton ci-dessous pour revenir sur la page d'accueil.", // "404.link.home-page": "Take me to the home page", - // TODO New key - Add a translation - "404.link.home-page": "Take me to the home page", - + "404.link.home-page": "Retour sur la page d'accueil", // "404.page-not-found": "page not found", - // TODO New key - Add a translation - "404.page-not-found": "page not found", - - + "404.page-not-found": "Page introuvable", // "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", - + "admin.registries.bitstream-formats.create.failure.content": "Une erreur s'est produite lors de l'ajout du nouveau format Bitstream.", // "admin.registries.bitstream-formats.create.failure.head": "Failure", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.failure.head": "Failure", - + "admin.registries.bitstream-formats.create.failure.head": "Erreur", // "admin.registries.bitstream-formats.create.head": "Create Bitstream format", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.head": "Create Bitstream format", - + "admin.registries.bitstream-formats.create.head": "Ajouter un format Bistream", // "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", - + "admin.registries.bitstream-formats.create.new": "Ajouter un format Bitstream", // "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", - + "admin.registries.bitstream-formats.create.success.content": "Le nouveau format Bitstream a été ajouté avec succès.", // "admin.registries.bitstream-formats.create.success.head": "Success", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.success.head": "Success", - + "admin.registries.bitstream-formats.create.success.head": "Succès", // "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", - + "admin.registries.bitstream-formats.delete.failure.amount": "La suppression de {{ amount }} format(s) a échoué", // "admin.registries.bitstream-formats.delete.failure.head": "Failure", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.failure.head": "Failure", - + "admin.registries.bitstream-formats.delete.failure.head": "Échec", // "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", - + "admin.registries.bitstream-formats.delete.success.amount": "{{ amount }} format(s) ajouté(s) avec succès", // "admin.registries.bitstream-formats.delete.success.head": "Success", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.success.head": "Success", - + "admin.registries.bitstream-formats.delete.success.head": "Succès", // "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", - + "admin.registries.bitstream-formats.description": "Cette liste des formats Bitstream contient les informations relatives aux formats connus et leur niveau de support.", // "admin.registries.bitstream-formats.edit.description.hint": "", - // TODO New key - Add a translation "admin.registries.bitstream-formats.edit.description.hint": "", - // "admin.registries.bitstream-formats.edit.description.label": "Description", - // TODO New key - Add a translation "admin.registries.bitstream-formats.edit.description.label": "Description", - // "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.", - // TODO New key - Add a translation - "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": "Les extensions de fichier sont utilisées pour identifier le format des fichiers téléchargés. Plusieurs extensions peuvent être encodées pour chaque format.", // "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", - + "admin.registries.bitstream-formats.edit.extensions.label": "Extensions de fichier", // "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot", - + "admin.registries.bitstream-formats.edit.extensions.placeholder": "Veuillez entrer l'extension de fichier sans point", // "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", - + "admin.registries.bitstream-formats.edit.failure.content": "Une erreur s'est produite lors de l'édition du format Bitstream.", // "admin.registries.bitstream-formats.edit.failure.head": "Failure", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.failure.head": "Failure", - + "admin.registries.bitstream-formats.edit.failure.head": "Échec", // "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", - + "admin.registries.bitstream-formats.edit.head": "Format Bitstream: {{ format }}", // "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are are hidden from the user, and used for administrative purposes.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are are hidden from the user, and used for administrative purposes.", - + "admin.registries.bitstream-formats.edit.internal.hint": "Les formats identifiés en tant que formats internes sont cachés pour l'utilisateur et utilisés uniquement à des fins administratives.", // "admin.registries.bitstream-formats.edit.internal.label": "Internal", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.internal.label": "Internal", - + "admin.registries.bitstream-formats.edit.internal.label": "Interne", // "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", - + "admin.registries.bitstream-formats.edit.mimetype.hint": "Le type MIME associé avec ce format ne doit pas nécessairement être unique.", // "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", - + "admin.registries.bitstream-formats.edit.mimetype.label": "Type MIME", // "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", - + "admin.registries.bitstream-formats.edit.shortDescription.hint": "Nom unique pour ce format (p. ex. Microsoft Word XP or Microsoft Word 2000)", // "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", - + "admin.registries.bitstream-formats.edit.shortDescription.label": "Nom", // "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", - + "admin.registries.bitstream-formats.edit.success.content": "Le format Bitstream a été édité avec succès.", // "admin.registries.bitstream-formats.edit.success.head": "Success", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.success.head": "Success", - + "admin.registries.bitstream-formats.edit.success.head": "Succès", // "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", - + "admin.registries.bitstream-formats.edit.supportLevel.hint": "Le niveau de support auquel votre institution souscrit pour ce format.", // "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", - + "admin.registries.bitstream-formats.edit.supportLevel.label": "Niveau de support", // "admin.registries.bitstream-formats.head": "Bitstream Format Registry", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.head": "Bitstream Format Registry", - + "admin.registries.bitstream-formats.head": "Registre des formats Bitstream", // "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", - + "admin.registries.bitstream-formats.no-items": "Aucun format Bitstream disponible.", // "admin.registries.bitstream-formats.table.delete": "Delete selected", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.delete": "Delete selected", - + "admin.registries.bitstream-formats.table.delete": "Supprimer les éléments sélectionnés", // "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", - + "admin.registries.bitstream-formats.table.deselect-all": "Désélectionner tout", // "admin.registries.bitstream-formats.table.internal": "internal", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.internal": "internal", - + "admin.registries.bitstream-formats.table.internal": "interne", // "admin.registries.bitstream-formats.table.mimetype": "MIME Type", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.mimetype": "MIME Type", - + "admin.registries.bitstream-formats.table.mimetype": "Type MIME", // "admin.registries.bitstream-formats.table.name": "Name", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.name": "Name", - + "admin.registries.bitstream-formats.table.name": "Nom", // "admin.registries.bitstream-formats.table.return": "Return", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.return": "Return", - + "admin.registries.bitstream-formats.table.return": "Retour", // "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", - + "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Connu", // "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", - + "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supporté", // "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", - + "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Inconnu", // "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", - + "admin.registries.bitstream-formats.table.supportLevel.head": "Niveau de support", // "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", - - + "admin.registries.bitstream-formats.title": "DSpace Angular :: Registre des formats Bitstream", // "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.", - // TODO New key - Add a translation - "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": "Le registre de métadonnées présente une liste à jour de tous les champs de métadonnées disponibles dans le dépôt. Ces champs peuvent être répartis entre plusieurs schémas. Le schéma Dublin Core qualifié est toutefois requis dans DSpace.", // "admin.registries.metadata.form.create": "Create metadata schema", - // TODO New key - Add a translation - "admin.registries.metadata.form.create": "Create metadata schema", - + "admin.registries.metadata.form.create": "Ajouter un schéma de métadonnées", // "admin.registries.metadata.form.edit": "Edit metadata schema", - // TODO New key - Add a translation - "admin.registries.metadata.form.edit": "Edit metadata schema", - + "admin.registries.metadata.form.edit": "Éditer un schéma de métadonnées", // "admin.registries.metadata.form.name": "Name", - // TODO New key - Add a translation - "admin.registries.metadata.form.name": "Name", - + "admin.registries.metadata.form.name": "Nom", // "admin.registries.metadata.form.namespace": "Namespace", - // TODO New key - Add a translation - "admin.registries.metadata.form.namespace": "Namespace", - + "admin.registries.metadata.form.namespace": "Espace de nommage", // "admin.registries.metadata.head": "Metadata Registry", - // TODO New key - Add a translation - "admin.registries.metadata.head": "Metadata Registry", - + "admin.registries.metadata.head": "Registre de métadonnées", // "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", - + "admin.registries.metadata.schemas.no-items": "Aucun schéma de métadonnées disponible.", // "admin.registries.metadata.schemas.table.delete": "Delete selected", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.table.delete": "Delete selected", - + "admin.registries.metadata.schemas.table.delete": "Supprimer les éléments sélectionnés", // "admin.registries.metadata.schemas.table.id": "ID", - // TODO New key - Add a translation "admin.registries.metadata.schemas.table.id": "ID", - // "admin.registries.metadata.schemas.table.name": "Name", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.table.name": "Name", - + "admin.registries.metadata.schemas.table.name": "Nom", // "admin.registries.metadata.schemas.table.namespace": "Namespace", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.table.namespace": "Namespace", - + "admin.registries.metadata.schemas.table.namespace": "Espace de nommage", // "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", - // TODO New key - Add a translation - "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", - - - + "admin.registries.metadata.title": "DSpace Angular :: Registre de métadonnées", // "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", - // TODO New key - Add a translation - "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", - + "admin.registries.schema.description": "Ceci est le schéma de métadonnées correspondant à l'espace de nommage « {{namespace}} ».", // "admin.registries.schema.fields.head": "Schema metadata fields", - // TODO New key - Add a translation - "admin.registries.schema.fields.head": "Schema metadata fields", - + "admin.registries.schema.fields.head": "Champs de métadonnées du schéma", // "admin.registries.schema.fields.no-items": "No metadata fields to show.", - // TODO New key - Add a translation - "admin.registries.schema.fields.no-items": "No metadata fields to show.", - + "admin.registries.schema.fields.no-items": "Aucun champ de métadonnée disponible.", // "admin.registries.schema.fields.table.delete": "Delete selected", - // TODO New key - Add a translation - "admin.registries.schema.fields.table.delete": "Delete selected", - + "admin.registries.schema.fields.table.delete": "Supprimer les éléments sélectionnés", // "admin.registries.schema.fields.table.field": "Field", - // TODO New key - Add a translation - "admin.registries.schema.fields.table.field": "Field", - + "admin.registries.schema.fields.table.field": "Champ", // "admin.registries.schema.fields.table.scopenote": "Scope Note", - // TODO New key - Add a translation - "admin.registries.schema.fields.table.scopenote": "Scope Note", - + "admin.registries.schema.fields.table.scopenote": "Note d'application", // "admin.registries.schema.form.create": "Create metadata field", - // TODO New key - Add a translation - "admin.registries.schema.form.create": "Create metadata field", - + "admin.registries.schema.form.create": "Ajouter un champ de métadonnées", // "admin.registries.schema.form.edit": "Edit metadata field", - // TODO New key - Add a translation - "admin.registries.schema.form.edit": "Edit metadata field", - + "admin.registries.schema.form.edit": "Modifier champ de métadonnées", // "admin.registries.schema.form.element": "Element", - // TODO New key - Add a translation - "admin.registries.schema.form.element": "Element", - + "admin.registries.schema.form.element": "Élément", // "admin.registries.schema.form.qualifier": "Qualifier", - // TODO New key - Add a translation - "admin.registries.schema.form.qualifier": "Qualifier", - + "admin.registries.schema.form.qualifier": "Qualificatif", // "admin.registries.schema.form.scopenote": "Scope Note", - // TODO New key - Add a translation - "admin.registries.schema.form.scopenote": "Scope Note", - + "admin.registries.schema.form.scopenote": "Note d'application", // "admin.registries.schema.head": "Metadata Schema", - // TODO New key - Add a translation - "admin.registries.schema.head": "Metadata Schema", - + "admin.registries.schema.head": "Schéma de métadonnées", // "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", - // TODO New key - Add a translation - "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", - + "admin.registries.schema.notification.created": "Schéma de métadonnées « {{prefix}} » créé avec succès", // "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", - // TODO New key - Add a translation - "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", - + "admin.registries.schema.notification.deleted.failure": "Erreur lors de la suppression de {{amount}} schéma(s) de métadonnées", // "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", - // TODO New key - Add a translation - "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", - + "admin.registries.schema.notification.deleted.success": "{{amount}} schéma(s) de métadonnées supprimé(s) avec succès", // "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", - // TODO New key - Add a translation - "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", - + "admin.registries.schema.notification.edited": "Schéma de métadonnées « {{prefix}} » édité avec succès", // "admin.registries.schema.notification.failure": "Error", - // TODO New key - Add a translation - "admin.registries.schema.notification.failure": "Error", - + "admin.registries.schema.notification.failure": "Erreur", // "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", - + "admin.registries.schema.notification.field.created": "Champ de métadonnée « {{field}} » créé avec succès", // "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", - + "admin.registries.schema.notification.field.deleted.failure": "Erreur lors de la suppression de {{amount}} champ(s) de métadonnées", // "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", - + "admin.registries.schema.notification.field.deleted.success": "{{amount}} champ(s) de métadonnées supprimé(s) avec succès", // "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", - + "admin.registries.schema.notification.field.edited": "Champ de métadonnée « {{field}} » édité avec succès", // "admin.registries.schema.notification.success": "Success", - // TODO New key - Add a translation - "admin.registries.schema.notification.success": "Success", - + "admin.registries.schema.notification.success": "Succès", // "admin.registries.schema.return": "Return", - // TODO New key - Add a translation - "admin.registries.schema.return": "Return", - + "admin.registries.schema.return": "Retour", // "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", - // TODO New key - Add a translation - "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", - - + "admin.registries.schema.title": "DSpace Angular :: Registre des schémas de métadonnées", // "auth.errors.invalid-user": "Invalid email address or password.", - // TODO New key - Add a translation - "auth.errors.invalid-user": "Invalid email address or password.", - + "auth.errors.invalid-user": "Adresse e-mail ou mot de passe non valide.", // "auth.messages.expired": "Your session has expired. Please log in again.", - // TODO New key - Add a translation - "auth.messages.expired": "Your session has expired. Please log in again.", - - + "auth.messages.expired": "Votre session a expiré. Veuillez vous reconnecter.", // "browse.comcol.by.author": "By Author", - // TODO New key - Add a translation - "browse.comcol.by.author": "By Author", - + "browse.comcol.by.author": "Auteur", // "browse.comcol.by.dateissued": "By Issue Date", - // TODO New key - Add a translation - "browse.comcol.by.dateissued": "By Issue Date", - + "browse.comcol.by.dateissued": "Date de publication", // "browse.comcol.by.subject": "By Subject", - // TODO New key - Add a translation - "browse.comcol.by.subject": "By Subject", - + "browse.comcol.by.subject": "Sujet", // "browse.comcol.by.title": "By Title", - // TODO New key - Add a translation - "browse.comcol.by.title": "By Title", - + "browse.comcol.by.title": "Titre", // "browse.comcol.head": "Browse", - // TODO New key - Add a translation - "browse.comcol.head": "Browse", - + "browse.comcol.head": "Parcourir par", // "browse.empty": "No items to show.", - // TODO New key - Add a translation - "browse.empty": "No items to show.", - + "browse.empty": "Aucun résultat.", // "browse.metadata.author": "Author", - // TODO New key - Add a translation - "browse.metadata.author": "Author", - + "browse.metadata.author": "Auteur", // "browse.metadata.dateissued": "Issue Date", - // TODO New key - Add a translation - "browse.metadata.dateissued": "Issue Date", - + "browse.metadata.dateissued": "Date de publication", // "browse.metadata.subject": "Subject", - // TODO New key - Add a translation - "browse.metadata.subject": "Subject", - + "browse.metadata.subject": "Sujet", // "browse.metadata.title": "Title", - // TODO New key - Add a translation - "browse.metadata.title": "Title", - + "browse.metadata.title": "Titre", // "browse.startsWith.choose_start": "(Choose start)", - // TODO New key - Add a translation - "browse.startsWith.choose_start": "(Choose start)", - + "browse.startsWith.choose_start": "(Choisir point de départ)", // "browse.startsWith.choose_year": "(Choose year)", - // TODO New key - Add a translation - "browse.startsWith.choose_year": "(Choose year)", - + "browse.startsWith.choose_year": "(Choisir l'année)", // "browse.startsWith.jump": "Jump to a point in the index:", - // TODO New key - Add a translation - "browse.startsWith.jump": "Jump to a point in the index:", - + "browse.startsWith.jump": "Aller directement à une entrée de l'index :", // "browse.startsWith.months.april": "April", - // TODO New key - Add a translation - "browse.startsWith.months.april": "April", - + "browse.startsWith.months.april": "Avril", // "browse.startsWith.months.august": "August", - // TODO New key - Add a translation - "browse.startsWith.months.august": "August", - + "browse.startsWith.months.august": "Août", // "browse.startsWith.months.december": "December", - // TODO New key - Add a translation - "browse.startsWith.months.december": "December", - + "browse.startsWith.months.december": "Décembre", // "browse.startsWith.months.february": "February", - // TODO New key - Add a translation - "browse.startsWith.months.february": "February", - + "browse.startsWith.months.february": "Février", // "browse.startsWith.months.january": "January", - // TODO New key - Add a translation - "browse.startsWith.months.january": "January", - + "browse.startsWith.months.january": "Janvier", // "browse.startsWith.months.july": "July", - // TODO New key - Add a translation - "browse.startsWith.months.july": "July", - + "browse.startsWith.months.july": "Juillet", // "browse.startsWith.months.june": "June", - // TODO New key - Add a translation - "browse.startsWith.months.june": "June", - + "browse.startsWith.months.june": "Juin", // "browse.startsWith.months.march": "March", - // TODO New key - Add a translation - "browse.startsWith.months.march": "March", - + "browse.startsWith.months.march": "Mars", // "browse.startsWith.months.may": "May", - // TODO New key - Add a translation - "browse.startsWith.months.may": "May", - + "browse.startsWith.months.may": "Mai", // "browse.startsWith.months.none": "(Choose month)", - // TODO New key - Add a translation - "browse.startsWith.months.none": "(Choose month)", - + "browse.startsWith.months.none": "(Choisir le mois)", // "browse.startsWith.months.november": "November", - // TODO New key - Add a translation - "browse.startsWith.months.november": "November", - + "browse.startsWith.months.november": "Novembre", // "browse.startsWith.months.october": "October", - // TODO New key - Add a translation - "browse.startsWith.months.october": "October", - + "browse.startsWith.months.october": "Octobre", // "browse.startsWith.months.september": "September", - // TODO New key - Add a translation - "browse.startsWith.months.september": "September", - + "browse.startsWith.months.september": "Septembre", // "browse.startsWith.submit": "Go", - // TODO New key - Add a translation - "browse.startsWith.submit": "Go", - + "browse.startsWith.submit": "Valider", // "browse.startsWith.type_date": "Or type in a date (year-month):", - // TODO New key - Add a translation - "browse.startsWith.type_date": "Or type in a date (year-month):", - + "browse.startsWith.type_date": "Ou saisir une date (année-mois) :", // "browse.startsWith.type_text": "Or enter first few letters:", - // TODO New key - Add a translation - "browse.startsWith.type_text": "Or enter first few letters:", - + "browse.startsWith.type_text": "Ou saisir les premières lettres :", // "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", - // TODO New key - Add a translation - "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", - - + "browse.title": "Parcourir {{ collection }} par {{ field }} {{ value }}", // "chips.remove": "Remove chip", - // TODO New key - Add a translation - "chips.remove": "Remove chip", - - + "chips.remove": "Supprimer fragment", // "collection.create.head": "Create a Collection", - // TODO New key - Add a translation - "collection.create.head": "Create a Collection", - + "collection.create.head": "Créer une Collection", // "collection.create.sub-head": "Create a Collection for Community {{ parent }}", - // TODO New key - Add a translation - "collection.create.sub-head": "Create a Collection for Community {{ parent }}", - + "collection.create.sub-head": "Créer une Collection au sein de la Communauté {{ parent }}", // "collection.delete.cancel": "Cancel", - // TODO New key - Add a translation - "collection.delete.cancel": "Cancel", - + "collection.delete.cancel": "Annuler", // "collection.delete.confirm": "Confirm", - // TODO New key - Add a translation - "collection.delete.confirm": "Confirm", - + "collection.delete.confirm": "Valider", // "collection.delete.head": "Delete Collection", - // TODO New key - Add a translation - "collection.delete.head": "Delete Collection", - + "collection.delete.head": "Supprimer Collection", // "collection.delete.notification.fail": "Collection could not be deleted", - // TODO New key - Add a translation - "collection.delete.notification.fail": "Collection could not be deleted", - + "collection.delete.notification.fail": "La Collection n'a pas pu être supprimée", // "collection.delete.notification.success": "Successfully deleted collection", - // TODO New key - Add a translation - "collection.delete.notification.success": "Successfully deleted collection", - + "collection.delete.notification.success": "Collection supprimée avec succès", // "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", - // TODO New key - Add a translation - "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", - - - + "collection.delete.text": "Êtes-vous certain de vouloir supprimer la Collection « {{ dso }} »", // "collection.edit.delete": "Delete this collection", - // TODO New key - Add a translation - "collection.edit.delete": "Delete this collection", - + "collection.edit.delete": "Supprimer cette Collection", // "collection.edit.head": "Edit Collection", - // TODO New key - Add a translation - "collection.edit.head": "Edit Collection", - - - + "collection.edit.head": "Éditer Collection", // "collection.edit.item-mapper.cancel": "Cancel", - // TODO New key - Add a translation - "collection.edit.item-mapper.cancel": "Cancel", - + "collection.edit.item-mapper.cancel": "Annuler", // "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", - // TODO New key - Add a translation - "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", - + "collection.edit.item-mapper.collection": "Collection : « {{name}} »", // "collection.edit.item-mapper.confirm": "Map selected items", - // TODO New key - Add a translation - "collection.edit.item-mapper.confirm": "Map selected items", - + "collection.edit.item-mapper.confirm": "Associer les Items sélectionnés", // "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", - + "collection.edit.item-mapper.description": "L'Item Mapper permet aux administrateurs de Collection d'associer des Items provenant d'autres Collections dans cette Collection. Vous pouvez rechercher des Items d'autres Collections et les associer, ou consulter la liste des Items actuellement associés.", // "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", - // TODO New key - Add a translation - "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", - + "collection.edit.item-mapper.head": "Item Mapper - Associer des Items provenant d'autres Collections", // "collection.edit.item-mapper.no-search": "Please enter a query to search", - // TODO New key - Add a translation - "collection.edit.item-mapper.no-search": "Please enter a query to search", - + "collection.edit.item-mapper.no-search": "Veuillez entrer un élément à rechercher", // "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", - + "collection.edit.item-mapper.notifications.map.error.content": "Des erreurs se sont produites lors de l'association de {{amount}} Item(s).", // "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", - + "collection.edit.item-mapper.notifications.map.error.head": "Erreurs d'association", // "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", - + "collection.edit.item-mapper.notifications.map.success.content": "{{amount}} Item(s) associé(s) avec succès.", // "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", - + "collection.edit.item-mapper.notifications.map.success.head": "Association terminée", // "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", - + "collection.edit.item-mapper.notifications.unmap.error.content": "Des erreurs se sont produites lors de la suppression de l'association de {{amount}} Item(s).", // "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", - + "collection.edit.item-mapper.notifications.unmap.error.head": "Erreurs de suppression d'association", // "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", - + "collection.edit.item-mapper.notifications.unmap.success.content": "{{amount}} association(s) supprimée(s) avec succès.", // "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", - + "collection.edit.item-mapper.notifications.unmap.success.head": "Suppression d'association terminée", // "collection.edit.item-mapper.remove": "Remove selected item mappings", - // TODO New key - Add a translation - "collection.edit.item-mapper.remove": "Remove selected item mappings", - + "collection.edit.item-mapper.remove": "Supprimer les associations sélectionnées", // "collection.edit.item-mapper.tabs.browse": "Browse mapped items", - // TODO New key - Add a translation - "collection.edit.item-mapper.tabs.browse": "Browse mapped items", - + "collection.edit.item-mapper.tabs.browse": "Parcourir les Items associés", // "collection.edit.item-mapper.tabs.map": "Map new items", - // TODO New key - Add a translation - "collection.edit.item-mapper.tabs.map": "Map new items", - - + "collection.edit.item-mapper.tabs.map": "Associer nouveaux Items", // "collection.form.abstract": "Short Description", - // TODO New key - Add a translation - "collection.form.abstract": "Short Description", - + "collection.form.abstract": "Description succincte", // "collection.form.description": "Introductory text (HTML)", - // TODO New key - Add a translation - "collection.form.description": "Introductory text (HTML)", - + "collection.form.description": "Texte d'introduction (HTML)", // "collection.form.errors.title.required": "Please enter a collection name", - // TODO New key - Add a translation - "collection.form.errors.title.required": "Please enter a collection name", - + "collection.form.errors.title.required": "Veuillez entrer un nom de Collection", // "collection.form.license": "License", - // TODO New key - Add a translation - "collection.form.license": "License", - + "collection.form.license": "Licence", // "collection.form.provenance": "Provenance", - // TODO New key - Add a translation "collection.form.provenance": "Provenance", - // "collection.form.rights": "Copyright text (HTML)", - // TODO New key - Add a translation - "collection.form.rights": "Copyright text (HTML)", - + "collection.form.rights": "Texte de copyright (HTML)", // "collection.form.tableofcontents": "News (HTML)", - // TODO New key - Add a translation - "collection.form.tableofcontents": "News (HTML)", - + "collection.form.tableofcontents": "Nouvelles (HTML)", // "collection.form.title": "Name", - // TODO New key - Add a translation - "collection.form.title": "Name", - - - + "collection.form.title": "Nom", // "collection.page.browse.recent.head": "Recent Submissions", - // TODO New key - Add a translation - "collection.page.browse.recent.head": "Recent Submissions", - + "collection.page.browse.recent.head": "Dépôts récents", // "collection.page.browse.recent.empty": "No items to show", - // TODO New key - Add a translation - "collection.page.browse.recent.empty": "No items to show", - + "collection.page.browse.recent.empty": "Aucun dépôt disponible", // "collection.page.handle": "Permanent URI for this collection", - // TODO New key - Add a translation - "collection.page.handle": "Permanent URI for this collection", - + "collection.page.handle": "URI permanent de cette Collection", // "collection.page.license": "License", - // TODO New key - Add a translation - "collection.page.license": "License", - + "collection.page.license": "Licence", // "collection.page.news": "News", - // TODO New key - Add a translation - "collection.page.news": "News", - - + "collection.page.news": "Nouvelles", // "collection.select.confirm": "Confirm selected", - // TODO New key - Add a translation - "collection.select.confirm": "Confirm selected", - + "collection.select.confirm": "Confirmer sélection", // "collection.select.empty": "No collections to show", - // TODO New key - Add a translation - "collection.select.empty": "No collections to show", - + "collection.select.empty": "Aucune Collection disponible", // "collection.select.table.title": "Title", - // TODO New key - Add a translation - "collection.select.table.title": "Title", - - + "collection.select.table.title": "Titre", // "community.create.head": "Create a Community", - // TODO New key - Add a translation - "community.create.head": "Create a Community", - + "community.create.head": "Créer une Communauté", // "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", - // TODO New key - Add a translation - "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", + "community.create.sub-head": "Créer une Sous-Communauté pour la Communauté {{ parent }}", // "community.delete.cancel": "Cancel", - // TODO New key - Add a translation - "community.delete.cancel": "Cancel", - + "community.delete.cancel": "Annuler", // "community.delete.confirm": "Confirm", - // TODO New key - Add a translation - "community.delete.confirm": "Confirm", - + "community.delete.confirm": "Confirmer", // "community.delete.head": "Delete Community", - // TODO New key - Add a translation - "community.delete.head": "Delete Community", - + "community.delete.head": "Supprimer Communauté", // "community.delete.notification.fail": "Community could not be deleted", - // TODO New key - Add a translation - "community.delete.notification.fail": "Community could not be deleted", - + "community.delete.notification.fail": "La Communauté n'a pas pu être supprimée", // "community.delete.notification.success": "Successfully deleted community", - // TODO New key - Add a translation - "community.delete.notification.success": "Successfully deleted community", - + "community.delete.notification.success": "La Communauté a été supprimée avec succès", // "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", - // TODO New key - Add a translation - "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", + "community.delete.text": "Êtes-vous sûr(e) de vouloir supprimer la Communauté « {{ dso }} »", // "community.edit.delete": "Delete this community", - // TODO New key - Add a translation - "community.edit.delete": "Delete this community", - + "community.edit.delete": "Supprimer cette Communauté", // "community.edit.head": "Edit Community", - // TODO New key - Add a translation - "community.edit.head": "Edit Community", + "community.edit.head": "Éditer Communauté", // "community.form.abstract": "Short Description", - // TODO New key - Add a translation - "community.form.abstract": "Short Description", - + "community.form.abstract": "Description succincte", // "community.form.description": "Introductory text (HTML)", - // TODO New key - Add a translation - "community.form.description": "Introductory text (HTML)", - + "community.form.description": "Texte d'introduction (HTML)", // "community.form.errors.title.required": "Please enter a community name", - // TODO New key - Add a translation - "community.form.errors.title.required": "Please enter a community name", - + "community.form.errors.title.required": "Veuillez entrer un nom de Communauté", // "community.form.rights": "Copyright text (HTML)", - // TODO New key - Add a translation - "community.form.rights": "Copyright text (HTML)", - + "community.form.rights": "Texte de copyright (HTML)", // "community.form.tableofcontents": "News (HTML)", - // TODO New key - Add a translation - "community.form.tableofcontents": "News (HTML)", - + "community.form.tableofcontents": "Nouvelles (HTML)", // "community.form.title": "Name", - // TODO New key - Add a translation - "community.form.title": "Name", + "community.form.title": "Nom", // "community.page.handle": "Permanent URI for this community", - // TODO New key - Add a translation - "community.page.handle": "Permanent URI for this community", - + "community.page.handle": "URI permanent de cette Communauté", // "community.page.license": "License", - // TODO New key - Add a translation - "community.page.license": "License", - + "community.page.license": "Licence", // "community.page.news": "News", - // TODO New key - Add a translation - "community.page.news": "News", + "community.page.news": "Nouvelles", // "community.all-lists.head": "Subcommunities and Collections", - // TODO New key - Add a translation - "community.all-lists.head": "Subcommunities and Collections", - + "community.all-lists.head": "Sous-Communautés et Collections", // "community.sub-collection-list.head": "Collections of this Community", - // TODO New key - Add a translation - "community.sub-collection-list.head": "Collections of this Community", - + "community.sub-collection-list.head": "Collections au sein de cette Communauté", // "community.sub-community-list.head": "Communities of this Community", - // TODO New key - Add a translation - "community.sub-community-list.head": "Communities of this Community", - - + "community.sub-community-list.head": "Sous-Communautés au sein de cette Communauté", // "dso-selector.create.collection.head": "New collection", - // TODO New key - Add a translation - "dso-selector.create.collection.head": "New collection", - + "dso-selector.create.collection.head": "Nouvelle Collection", // "dso-selector.create.community.head": "New community", - // TODO New key - Add a translation - "dso-selector.create.community.head": "New community", - + "dso-selector.create.community.head": "Nouvelle Communauté", // "dso-selector.create.community.sub-level": "Create a new community in", - // TODO New key - Add a translation - "dso-selector.create.community.sub-level": "Create a new community in", - + "dso-selector.create.community.sub-level": "Créer une nouvelle Communauté dans", // "dso-selector.create.community.top-level": "Create a new top-level community", - // TODO New key - Add a translation - "dso-selector.create.community.top-level": "Create a new top-level community", - + "dso-selector.create.community.top-level": "Créer une nouvelle Communauté de 1er niveau", // "dso-selector.create.item.head": "New item", - // TODO New key - Add a translation - "dso-selector.create.item.head": "New item", - + "dso-selector.create.item.head": "Nouvel Item", // "dso-selector.edit.collection.head": "Edit collection", - // TODO New key - Add a translation - "dso-selector.edit.collection.head": "Edit collection", - + "dso-selector.edit.collection.head": "Éditer Collection", // "dso-selector.edit.community.head": "Edit community", - // TODO New key - Add a translation - "dso-selector.edit.community.head": "Edit community", - + "dso-selector.edit.community.head": "Éditer Communauté", // "dso-selector.edit.item.head": "Edit item", - // TODO New key - Add a translation - "dso-selector.edit.item.head": "Edit item", + "dso-selector.edit.item.head": "Éditer Item", // "dso-selector.no-results": "No {{ type }} found", - // TODO New key - Add a translation - "dso-selector.no-results": "No {{ type }} found", - + "dso-selector.no-results": "Aucun {{ type }} trouvé", // "dso-selector.placeholder": "Search for a {{ type }}", - // TODO New key - Add a translation - "dso-selector.placeholder": "Search for a {{ type }}", - - + "dso-selector.placeholder": "Rechercher un(e) {{ type }}", // "error.browse-by": "Error fetching items", - // TODO New key - Add a translation - "error.browse-by": "Error fetching items", - + "error.browse-by": "Erreur lors de la récupération des Items", // "error.collection": "Error fetching collection", - // TODO New key - Add a translation - "error.collection": "Error fetching collection", - + "error.collection": "Erreur lors de la récupération de la Collection", // "error.collections": "Error fetching collections", - // TODO New key - Add a translation - "error.collections": "Error fetching collections", - + "error.collections": "Erreur lors de la récupération des Collections", // "error.community": "Error fetching community", - // TODO New key - Add a translation - "error.community": "Error fetching community", - + "error.community": "Erreur lors de la récupération de la Communauté", // "error.identifier": "No item found for the identifier", - // TODO New key - Add a translation - "error.identifier": "No item found for the identifier", - + "error.identifier": "Aucun objet correspondant à cet identifiant", // "error.default": "Error", - // TODO New key - Add a translation - "error.default": "Error", - + "error.default": "Erreur", // "error.item": "Error fetching item", - // TODO New key - Add a translation - "error.item": "Error fetching item", - + "error.item": "Erreur lors de la récupération de l'Item", // "error.items": "Error fetching items", - // TODO New key - Add a translation - "error.items": "Error fetching items", - + "error.items": "Erreur lors de la récupération des Items", // "error.objects": "Error fetching objects", - // TODO New key - Add a translation - "error.objects": "Error fetching objects", - + "error.objects": "Erreur lors de la récupération des Objets", // "error.recent-submissions": "Error fetching recent submissions", - // TODO New key - Add a translation - "error.recent-submissions": "Error fetching recent submissions", - + "error.recent-submissions": "Erreur lors de la récupération des dépôts récents", // "error.search-results": "Error fetching search results", - // TODO New key - Add a translation - "error.search-results": "Error fetching search results", - + "error.search-results": "Erreur lors de la récupération des résultats de recherche", // "error.sub-collections": "Error fetching sub-collections", - // TODO New key - Add a translation - "error.sub-collections": "Error fetching sub-collections", - + "error.sub-collections": "Erreur lors de la récupération des Sous-Collections", // "error.sub-communities": "Error fetching sub-communities", - // TODO New key - Add a translation - "error.sub-communities": "Error fetching sub-communities", - + "error.sub-communities": "Erreur lors de la récupération des Sous-Communautés", // "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

", - // TODO New key - Add a translation - "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

", - + "error.submission.sections.init-form-error": "Une erreur s'est produite dans la section d'initialisation, veuillez vérifier la configuration de votre formulaire de dépôt. Plus de détails disponibles ci-dessous :

", // "error.top-level-communities": "Error fetching top-level communities", - // TODO New key - Add a translation - "error.top-level-communities": "Error fetching top-level communities", - + "error.top-level-communities": "Erreur lors de la récupération des Communautés de 1er niveau", // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - // TODO New key - Add a translation - "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - + "error.validation.license.notgranted": "Vous devez accepter cette licence pour terminer votre dépôt. Si vous êtes dans l'incapacité d'accepter cette licence actuellement, vous pouvez sauvegarder votre dépôt et y revenir ultérieurement ou le supprimer.", // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", - // TODO New key - Add a translation - "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", - - + "error.validation.pattern": "Cette entrée est invalide en vertu du pattern : {{ pattern }}.", // "footer.copyright": "copyright © 2002-{{ year }}", // TODO New key - Add a translation - "footer.copyright": "copyright © 2002-{{ year }}", - + "footer.copyright": "Copyright © 2002-{{ year }}", // "footer.link.dspace": "DSpace software", - // TODO New key - Add a translation "footer.link.dspace": "DSpace software", - // "footer.link.duraspace": "DuraSpace", - // TODO New key - Add a translation "footer.link.duraspace": "DuraSpace", - - // "form.cancel": "Cancel", - // TODO New key - Add a translation - "form.cancel": "Cancel", - + "form.cancel": "Annuler", // "form.clear": "Clear", - // TODO New key - Add a translation - "form.clear": "Clear", - + "form.clear": "Effacer", // "form.clear-help": "Click here to remove the selected value", - // TODO New key - Add a translation - "form.clear-help": "Click here to remove the selected value", - + "form.clear-help": "Cliquer ici pour supprimer la valeur sélectionnée", // "form.edit": "Edit", - // TODO New key - Add a translation - "form.edit": "Edit", - + "form.edit": "Éditer", // "form.edit-help": "Click here to edit the selected value", - // TODO New key - Add a translation - "form.edit-help": "Click here to edit the selected value", - + "form.edit-help": "Cliquer ici pour éditer la valeur sélectionnée", // "form.first-name": "First name", - // TODO New key - Add a translation - "form.first-name": "First name", - + "form.first-name": "Prénom", // "form.group-collapse": "Collapse", - // TODO New key - Add a translation - "form.group-collapse": "Collapse", - + "form.group-collapse": "Réduire", // "form.group-collapse-help": "Click here to collapse", - // TODO New key - Add a translation - "form.group-collapse-help": "Click here to collapse", - + "form.group-collapse-help": "Cliquer ici pour réduire", // "form.group-expand": "Expand", - // TODO New key - Add a translation - "form.group-expand": "Expand", - + "form.group-expand": "Développer", // "form.group-expand-help": "Click here to expand and add more elements", - // TODO New key - Add a translation - "form.group-expand-help": "Click here to expand and add more elements", - + "form.group-expand-help": "Cliquer ici pour développer et ajouter davantage d'éléments", // "form.last-name": "Last name", - // TODO New key - Add a translation - "form.last-name": "Last name", - + "form.last-name": "Nom", // "form.loading": "Loading...", - // TODO New key - Add a translation - "form.loading": "Loading...", - + "form.loading": "En cours de chargement...", // "form.no-results": "No results found", - // TODO New key - Add a translation - "form.no-results": "No results found", - + "form.no-results": "Aucun résultat disponible", // "form.no-value": "No value entered", - // TODO New key - Add a translation - "form.no-value": "No value entered", - + "form.no-value": "Aucune valeur entrée", // "form.other-information": {}, - // TODO New key - Add a translation "form.other-information": {}, - // "form.remove": "Remove", - // TODO New key - Add a translation - "form.remove": "Remove", - + "form.remove": "Supprimer", // "form.save": "Save", - // TODO New key - Add a translation - "form.save": "Save", - + "form.save": "Sauvegarder", // "form.save-help": "Save changes", - // TODO New key - Add a translation - "form.save-help": "Save changes", - + "form.save-help": "Sauvegarder changements", // "form.search": "Search", - // TODO New key - Add a translation - "form.search": "Search", - + "form.search": "Rechercher", // "form.search-help": "Click here to looking for an existing correspondence", - // TODO New key - Add a translation - "form.search-help": "Click here to looking for an existing correspondence", - + "form.search-help": "Cliquer ici pour rechercher une correspondance existante", // "form.submit": "Submit", - // TODO New key - Add a translation - "form.submit": "Submit", - - + "form.submit": "Déposer", // "home.description": "", - // TODO New key - Add a translation "home.description": "", - // "home.title": "DSpace Angular :: Home", - // TODO New key - Add a translation - "home.title": "DSpace Angular :: Home", - + "home.title": "DSpace Angular :: Home", // "home.top-level-communities.head": "Communities in DSpace", - // TODO New key - Add a translation - "home.top-level-communities.head": "Communities in DSpace", - + "home.top-level-communities.head": "Communautés DSpace", // "home.top-level-communities.help": "Select a community to browse its collections.", - // TODO New key - Add a translation - "home.top-level-communities.help": "Select a community to browse its collections.", - - + "home.top-level-communities.help": "Sélectionner une Communauté pour parcourir les Collections sous-jacentes.", // "item.edit.delete.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.delete.cancel": "Cancel", - + "item.edit.delete.cancel": "Annuler", // "item.edit.delete.confirm": "Delete", - // TODO New key - Add a translation - "item.edit.delete.confirm": "Delete", - + "item.edit.delete.confirm": "Supprimer", // "item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.", - // TODO New key - Add a translation - "item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.", - + "item.edit.delete.description": "Êtes-vous certain de vouloir complètement supprimer cet Item ? Attention, il n'y aura pas de retour en arrière possible.", // "item.edit.delete.error": "An error occurred while deleting the item", - // TODO New key - Add a translation - "item.edit.delete.error": "An error occurred while deleting the item", - + "item.edit.delete.error": "Une erreur s'est produite lors de la suppression de l'Item", // "item.edit.delete.header": "Delete item: {{ id }}", - // TODO New key - Add a translation - "item.edit.delete.header": "Delete item: {{ id }}", - + "item.edit.delete.header": "Supprimer Item : {{ id }}", // "item.edit.delete.success": "The item has been deleted", - // TODO New key - Add a translation - "item.edit.delete.success": "The item has been deleted", - + "item.edit.delete.success": "L'Item a été supprimé", // "item.edit.head": "Edit Item", - // TODO New key - Add a translation - "item.edit.head": "Edit Item", - - - + "item.edit.head": "Éditer Item", // "item.edit.item-mapper.buttons.add": "Map item to selected collections", - // TODO New key - Add a translation - "item.edit.item-mapper.buttons.add": "Map item to selected collections", - + "item.edit.item-mapper.buttons.add": "Associer Item aux Collections sélectionnées", // "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", - // TODO New key - Add a translation - "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", - + "item.edit.item-mapper.buttons.remove": "Supprimer l'association d'Item pour les Collections sélectionnées", // "item.edit.item-mapper.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.item-mapper.cancel": "Cancel", - + "item.edit.item-mapper.cancel": "Annuler", // "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", - // TODO New key - Add a translation - "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", - + "item.edit.item-mapper.description": "L'Item Mapper permet aux administrateurs de Collection d'associer des Items avec d'autres Collections. Vous pouvez rechercher des Collections et les associer ou parcourir la liste des Collections auxquelles l'Item est actuellement associé.", // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", - // TODO New key - Add a translation - "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", - + "item.edit.item-mapper.head": "Item Mapper - Associer Item avec Collections", // "item.edit.item-mapper.item": "Item: \"{{name}}\"", - // TODO New key - Add a translation - "item.edit.item-mapper.item": "Item: \"{{name}}\"", - + "item.edit.item-mapper.item": "Item : « {{name}} »", // "item.edit.item-mapper.no-search": "Please enter a query to search", - // TODO New key - Add a translation - "item.edit.item-mapper.no-search": "Please enter a query to search", - + "item.edit.item-mapper.no-search": "Veuillez entrer un élément à rechercher", // "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", - + "item.edit.item-mapper.notifications.add.error.content": "Des erreurs se sont produites à l'association de l'Item avec {{amount}} Collections.", // "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", - + "item.edit.item-mapper.notifications.add.error.head": "Erreurs d'association", // "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", - + "item.edit.item-mapper.notifications.add.success.content": "Item associé avec succès avec {{amount}} Collections.", // "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", - + "item.edit.item-mapper.notifications.add.success.head": "Association terminée", // "item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.", - + "item.edit.item-mapper.notifications.remove.error.content": "Des erreurs se sont produites à la suppression de l'association de {{amount}} Collections.", // "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", - + "item.edit.item-mapper.notifications.remove.error.head": "Erreurs à la suppression d'association", // "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", - + "item.edit.item-mapper.notifications.remove.success.content": "Association de l'item avec {{amount}} Collections supprimée avec succès.", // "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", - + "item.edit.item-mapper.notifications.remove.success.head": "Suppression d'association terminée", // "item.edit.item-mapper.tabs.browse": "Browse mapped collections", - // TODO New key - Add a translation - "item.edit.item-mapper.tabs.browse": "Browse mapped collections", - + "item.edit.item-mapper.tabs.browse": "Parcourir Collections associées", // "item.edit.item-mapper.tabs.map": "Map new collections", - // TODO New key - Add a translation - "item.edit.item-mapper.tabs.map": "Map new collections", - - + "item.edit.item-mapper.tabs.map": "Associer nouvelles Collections", // "item.edit.metadata.add-button": "Add", - // TODO New key - Add a translation - "item.edit.metadata.add-button": "Add", - + "item.edit.metadata.add-button": "Ajouter", // "item.edit.metadata.discard-button": "Discard", - // TODO New key - Add a translation - "item.edit.metadata.discard-button": "Discard", - + "item.edit.metadata.discard-button": "Annuler", // "item.edit.metadata.edit.buttons.edit": "Edit", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.edit": "Edit", - + "item.edit.metadata.edit.buttons.edit": "Éditer", // "item.edit.metadata.edit.buttons.remove": "Remove", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.remove": "Remove", - + "item.edit.metadata.edit.buttons.remove": "Supprimer", // "item.edit.metadata.edit.buttons.undo": "Undo changes", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.undo": "Undo changes", - + "item.edit.metadata.edit.buttons.undo": "Annuler changements", // "item.edit.metadata.edit.buttons.unedit": "Stop editing", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.unedit": "Stop editing", - + "item.edit.metadata.edit.buttons.unedit": "Stopper l'édition", // "item.edit.metadata.headers.edit": "Edit", - // TODO New key - Add a translation - "item.edit.metadata.headers.edit": "Edit", - + "item.edit.metadata.headers.edit": "Éditer", // "item.edit.metadata.headers.field": "Field", - // TODO New key - Add a translation - "item.edit.metadata.headers.field": "Field", - + "item.edit.metadata.headers.field": "Champ", // "item.edit.metadata.headers.language": "Lang", - // TODO New key - Add a translation - "item.edit.metadata.headers.language": "Lang", - + "item.edit.metadata.headers.language": "Langue", // "item.edit.metadata.headers.value": "Value", - // TODO New key - Add a translation - "item.edit.metadata.headers.value": "Value", - + "item.edit.metadata.headers.value": "Valeur", // "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", - // TODO New key - Add a translation - "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", - + "item.edit.metadata.metadatafield.invalid": "Veuillez choisir un champ de métadonnée valide", // "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - // TODO New key - Add a translation - "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - + "item.edit.metadata.notifications.discarded.content": "Vos changements ont été annulés. Pour rétablir vos changements, cliquer sur le bouton 'Rétablir'", // "item.edit.metadata.notifications.discarded.title": "Changed discarded", - // TODO New key - Add a translation - "item.edit.metadata.notifications.discarded.title": "Changed discarded", - + "item.edit.metadata.notifications.discarded.title": "Changements annulés", // "item.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", - // TODO New key - Add a translation - "item.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", - + "item.edit.metadata.notifications.invalid.content": "Vos changements n'ont pas été sauvegardés. Veuillez vérifier que tous les champs sont valides avant de sauvegarder.", // "item.edit.metadata.notifications.invalid.title": "Metadata invalid", - // TODO New key - Add a translation - "item.edit.metadata.notifications.invalid.title": "Metadata invalid", - + "item.edit.metadata.notifications.invalid.title": "Métadonnée invalide", // "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - // TODO New key - Add a translation - "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - + "item.edit.metadata.notifications.outdated.content": "L'Item sur lequel vous êtes en train de travailler a été édité par un autre utilisateur. Vos changements en cours sont ignorés pour éviter des conflits", // "item.edit.metadata.notifications.outdated.title": "Changed outdated", - // TODO New key - Add a translation - "item.edit.metadata.notifications.outdated.title": "Changed outdated", - + "item.edit.metadata.notifications.outdated.title": "Changements obsolètes", // "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", - // TODO New key - Add a translation - "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", - + "item.edit.metadata.notifications.saved.content": "Vos modifications de métadonnées n'ont pas été sauvegardées.", // "item.edit.metadata.notifications.saved.title": "Metadata saved", - // TODO New key - Add a translation - "item.edit.metadata.notifications.saved.title": "Metadata saved", - + "item.edit.metadata.notifications.saved.title": "Métadonnée sauvegardée", // "item.edit.metadata.reinstate-button": "Undo", - // TODO New key - Add a translation - "item.edit.metadata.reinstate-button": "Undo", - + "item.edit.metadata.reinstate-button": "Rétablir", // "item.edit.metadata.save-button": "Save", - // TODO New key - Add a translation - "item.edit.metadata.save-button": "Save", - - + "item.edit.metadata.save-button": "Sauvegarder", // "item.edit.modify.overview.field": "Field", - // TODO New key - Add a translation - "item.edit.modify.overview.field": "Field", - + "item.edit.modify.overview.field": "Champ", // "item.edit.modify.overview.language": "Language", - // TODO New key - Add a translation - "item.edit.modify.overview.language": "Language", - + "item.edit.modify.overview.language": "Langue", // "item.edit.modify.overview.value": "Value", - // TODO New key - Add a translation - "item.edit.modify.overview.value": "Value", - - + "item.edit.modify.overview.value": "Valeur", // "item.edit.move.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.move.cancel": "Cancel", - + "item.edit.move.cancel": "Annuler", // "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", - // TODO New key - Add a translation - "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", - + "item.edit.move.description": "Sélectionner la Collection dans laquelle vous souhaitez transférer l'Item. Pour limiter la liste de Collections affichées, vous pouvez utiliser le champ de recherche.", // "item.edit.move.error": "An error occured when attempting to move the item", - // TODO New key - Add a translation - "item.edit.move.error": "An error occured when attempting to move the item", - + "item.edit.move.error": "Une erreur s'est produite lors du transfert de l'Item", // "item.edit.move.head": "Move item: {{id}}", - // TODO New key - Add a translation - "item.edit.move.head": "Move item: {{id}}", - + "item.edit.move.head": "Transférer item : {{id}}", // "item.edit.move.inheritpolicies.checkbox": "Inherit policies", - // TODO New key - Add a translation - "item.edit.move.inheritpolicies.checkbox": "Inherit policies", - + "item.edit.move.inheritpolicies.checkbox": "Récupérer règles", // "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", - // TODO New key - Add a translation - "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", - + "item.edit.move.inheritpolicies.description": "Récupérer les règles par défaut de la Collection de destination", // "item.edit.move.move": "Move", - // TODO New key - Add a translation - "item.edit.move.move": "Move", - + "item.edit.move.move": "Transférer", // "item.edit.move.processing": "Moving...", - // TODO New key - Add a translation - "item.edit.move.processing": "Moving...", - + "item.edit.move.processing": "En cours de transfert...", // "item.edit.move.search.placeholder": "Enter a search query to look for collections", - // TODO New key - Add a translation - "item.edit.move.search.placeholder": "Enter a search query to look for collections", - + "item.edit.move.search.placeholder": "Rechercher Collection", // "item.edit.move.success": "The item has been moved succesfully", - // TODO New key - Add a translation - "item.edit.move.success": "The item has been moved succesfully", - + "item.edit.move.success": "L'Item a été transféré avec succès", // "item.edit.move.title": "Move item", - // TODO New key - Add a translation - "item.edit.move.title": "Move item", - - + "item.edit.move.title": "Transférer item", // "item.edit.private.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.private.cancel": "Cancel", - + "item.edit.private.cancel": "Annuler", // "item.edit.private.confirm": "Make it Private", - // TODO New key - Add a translation - "item.edit.private.confirm": "Make it Private", - + "item.edit.private.confirm": "Définir comme Privé", // "item.edit.private.description": "Are you sure this item should be made private in the archive?", - // TODO New key - Add a translation - "item.edit.private.description": "Are you sure this item should be made private in the archive?", - + "item.edit.private.description": "Êtes-vous certain de vouloir définir cet Item comme Privé ?", // "item.edit.private.error": "An error occurred while making the item private", - // TODO New key - Add a translation - "item.edit.private.error": "An error occurred while making the item private", - + "item.edit.private.error": "Une erreur s'est produite lors de la définition de l'Item en tant qu'Item Privé", // "item.edit.private.header": "Make item private: {{ id }}", - // TODO New key - Add a translation - "item.edit.private.header": "Make item private: {{ id }}", - + "item.edit.private.header": "Définir Item comme Privé : {{ id }}", // "item.edit.private.success": "The item is now private", - // TODO New key - Add a translation - "item.edit.private.success": "The item is now private", - - + "item.edit.private.success": "L'Item est désormais Privé", // "item.edit.public.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.public.cancel": "Cancel", - + "item.edit.public.cancel": "Annuler", // "item.edit.public.confirm": "Make it Public", - // TODO New key - Add a translation - "item.edit.public.confirm": "Make it Public", - + "item.edit.public.confirm": "Définir comme Public", // "item.edit.public.description": "Are you sure this item should be made public in the archive?", - // TODO New key - Add a translation - "item.edit.public.description": "Are you sure this item should be made public in the archive?", - + "item.edit.public.description": "Êtes-vous certain de vouloir définir cet Item comme Public ?", // "item.edit.public.error": "An error occurred while making the item public", - // TODO New key - Add a translation - "item.edit.public.error": "An error occurred while making the item public", - + "item.edit.public.error": "Une erreur s'est produite lors e la définition de l'Item en tant qu'Item Public", // "item.edit.public.header": "Make item public: {{ id }}", - // TODO New key - Add a translation - "item.edit.public.header": "Make item public: {{ id }}", - + "item.edit.public.header": "Définir Item comme Public : {{ id }}", // "item.edit.public.success": "The item is now public", - // TODO New key - Add a translation - "item.edit.public.success": "The item is now public", - - + "item.edit.public.success": "L'Item est désormais Public", // "item.edit.reinstate.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.reinstate.cancel": "Cancel", - + "item.edit.reinstate.cancel": "Annuler", // "item.edit.reinstate.confirm": "Reinstate", - // TODO New key - Add a translation - "item.edit.reinstate.confirm": "Reinstate", - + "item.edit.reinstate.confirm": "Réintégrer", // "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", - // TODO New key - Add a translation - "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", - + "item.edit.reinstate.description": "Êtes-vous certain de vouloir réintégrer cet Item ?", // "item.edit.reinstate.error": "An error occurred while reinstating the item", - // TODO New key - Add a translation - "item.edit.reinstate.error": "An error occurred while reinstating the item", - + "item.edit.reinstate.error": "Une erreur s'est produite lors de la réintégration de l'Item", // "item.edit.reinstate.header": "Reinstate item: {{ id }}", - // TODO New key - Add a translation - "item.edit.reinstate.header": "Reinstate item: {{ id }}", - + "item.edit.reinstate.header": "Réintégrer Item : {{ id }}", // "item.edit.reinstate.success": "The item was reinstated successfully", - // TODO New key - Add a translation - "item.edit.reinstate.success": "The item was reinstated successfully", - - + "item.edit.reinstate.success": "L'Item a été réintégré avec succès", // "item.edit.relationships.discard-button": "Discard", - // TODO New key - Add a translation - "item.edit.relationships.discard-button": "Discard", - + "item.edit.relationships.discard-button": "Annuler", // "item.edit.relationships.edit.buttons.remove": "Remove", - // TODO New key - Add a translation - "item.edit.relationships.edit.buttons.remove": "Remove", - + "item.edit.relationships.edit.buttons.remove": "Supprimer", // "item.edit.relationships.edit.buttons.undo": "Undo changes", - // TODO New key - Add a translation - "item.edit.relationships.edit.buttons.undo": "Undo changes", - + "item.edit.relationships.edit.buttons.undo": "Annuler changements", // "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - // TODO New key - Add a translation - "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - + "item.edit.relationships.notifications.discarded.content": "Vos changements ont été annulés. Pour rétablir vos changements, cliquer sur le bouton 'Rétablir'", // "item.edit.relationships.notifications.discarded.title": "Changes discarded", - // TODO New key - Add a translation - "item.edit.relationships.notifications.discarded.title": "Changes discarded", - + "item.edit.relationships.notifications.discarded.title": "Changements annulés", // "item.edit.relationships.notifications.failed.title": "Error deleting relationship", - // TODO New key - Add a translation - "item.edit.relationships.notifications.failed.title": "Error deleting relationship", - + "item.edit.relationships.notifications.failed.title": "Erreur lors de la suppression de la Relation", // "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - // TODO New key - Add a translation - "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - + "item.edit.relationships.notifications.outdated.content": "L'Item sur lequel vous êtes en train de travailler a été édité par un autre utilisateur. Vos changements en cours sont ignorés pour éviter des conflits.", // "item.edit.relationships.notifications.outdated.title": "Changes outdated", - // TODO New key - Add a translation - "item.edit.relationships.notifications.outdated.title": "Changes outdated", - + "item.edit.relationships.notifications.outdated.title": "Changements obsolètes", // "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", - // TODO New key - Add a translation - "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", - + "item.edit.relationships.notifications.saved.content": "Vos changements aux Relations de l'Item ont été sauvegardés.", // "item.edit.relationships.notifications.saved.title": "Relationships saved", - // TODO New key - Add a translation - "item.edit.relationships.notifications.saved.title": "Relationships saved", - + "item.edit.relationships.notifications.saved.title": "Relations sauvegardées", // "item.edit.relationships.reinstate-button": "Undo", - // TODO New key - Add a translation - "item.edit.relationships.reinstate-button": "Undo", - + "item.edit.relationships.reinstate-button": "Rétablir", // "item.edit.relationships.save-button": "Save", - // TODO New key - Add a translation - "item.edit.relationships.save-button": "Save", - - - + "item.edit.relationships.save-button": "Sauvegarder", // "item.edit.tabs.bitstreams.head": "Item Bitstreams", - // TODO New key - Add a translation - "item.edit.tabs.bitstreams.head": "Item Bitstreams", - + "item.edit.tabs.bitstreams.head": "Bitstreams de l'Item", // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", - // TODO New key - Add a translation - "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", - + "item.edit.tabs.bitstreams.title": "Édition Item - Bitstreams", // "item.edit.tabs.curate.head": "Curate", - // TODO New key - Add a translation - "item.edit.tabs.curate.head": "Curate", - + "item.edit.tabs.curate.head": "Gestion de contenu", // "item.edit.tabs.curate.title": "Item Edit - Curate", - // TODO New key - Add a translation - "item.edit.tabs.curate.title": "Item Edit - Curate", - + "item.edit.tabs.curate.title": "Édition Item - Gestion de contenu", // "item.edit.tabs.metadata.head": "Item Metadata", - // TODO New key - Add a translation - "item.edit.tabs.metadata.head": "Item Metadata", - - // "item.edit.tabs.metadata.title": "Item Edit - Metadata", - // TODO New key - Add a translation - "item.edit.tabs.metadata.title": "Item Edit - Metadata", - + "item.edit.tabs.metadata.head": "Métadonnées de l'Item", + // "item.edit.tabs.metadata.title": "Item Edit - Metadata", + "item.edit.tabs.metadata.title": "Édition Item - Métadonnées", // "item.edit.tabs.relationships.head": "Item Relationships", - // TODO New key - Add a translation - "item.edit.tabs.relationships.head": "Item Relationships", - + "item.edit.tabs.relationships.head": "Relations de l'Item", // "item.edit.tabs.relationships.title": "Item Edit - Relationships", - // TODO New key - Add a translation - "item.edit.tabs.relationships.title": "Item Edit - Relationships", - + "item.edit.tabs.relationships.title": "Édition Item - Relations", // "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", - + "item.edit.tabs.status.buttons.authorizations.button": "Accès...", // "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", - + "item.edit.tabs.status.buttons.authorizations.label": "Éditer les règles d'accès de l'Item", // "item.edit.tabs.status.buttons.delete.button": "Permanently delete", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.delete.button": "Permanently delete", - + "item.edit.tabs.status.buttons.delete.button": "Supprimer définitivement", // "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", - + "item.edit.tabs.status.buttons.delete.label": "Supprimer l'Item définitivement", // "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", - + "item.edit.tabs.status.buttons.mappedCollections.button": "Collections associées", // "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", - + "item.edit.tabs.status.buttons.mappedCollections.label": "Gérer les Collections associées", // "item.edit.tabs.status.buttons.move.button": "Move...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.move.button": "Move...", - + "item.edit.tabs.status.buttons.move.button": "Transférer...", // "item.edit.tabs.status.buttons.move.label": "Move item to another collection", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.move.label": "Move item to another collection", - + "item.edit.tabs.status.buttons.move.label": "Transférer l'Item dans une autre Collection", // "item.edit.tabs.status.buttons.private.button": "Make it private...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.private.button": "Make it private...", - + "item.edit.tabs.status.buttons.private.button": "Définir comme privé...", // "item.edit.tabs.status.buttons.private.label": "Make item private", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.private.label": "Make item private", - + "item.edit.tabs.status.buttons.private.label": "Définir l'Item en tant qu'Item Privé", // "item.edit.tabs.status.buttons.public.button": "Make it public...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.public.button": "Make it public...", - + "item.edit.tabs.status.buttons.public.button": "Définir comme public...", // "item.edit.tabs.status.buttons.public.label": "Make item public", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.public.label": "Make item public", - + "item.edit.tabs.status.buttons.public.label": "Définir l'Item en tant qu'Item public", // "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", - + "item.edit.tabs.status.buttons.reinstate.button": "Réintégrer...", // "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", - + "item.edit.tabs.status.buttons.reinstate.label": "Réintégrer l'Item", // "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", - + "item.edit.tabs.status.buttons.withdraw.button": "Retirer...", // "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", - + "item.edit.tabs.status.buttons.withdraw.label": "Retirer l'Item", // "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", - // TODO New key - Add a translation - "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", - + "item.edit.tabs.status.description": "Bienvenue sur la page de gestion de l'Item. D'ici, vous pouvez retirer, réintégrer, transférer ou supprimer l'Item. Dans les autres onglets, vous pouvez également mettre à jour, ajouter ou supprimer des métadonnées et/ou bitstreams.", // "item.edit.tabs.status.head": "Item Status", - // TODO New key - Add a translation - "item.edit.tabs.status.head": "Item Status", - + "item.edit.tabs.status.head": "Statut Item", // "item.edit.tabs.status.labels.handle": "Handle", - // TODO New key - Add a translation "item.edit.tabs.status.labels.handle": "Handle", - // "item.edit.tabs.status.labels.id": "Item Internal ID", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.id": "Item Internal ID", - + "item.edit.tabs.status.labels.id": "Identifiant interne de l'Item", // "item.edit.tabs.status.labels.itemPage": "Item Page", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.itemPage": "Item Page", - + "item.edit.tabs.status.labels.itemPage": "Page de l'Item", // "item.edit.tabs.status.labels.lastModified": "Last Modified", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.lastModified": "Last Modified", - + "item.edit.tabs.status.labels.lastModified": "Date de dernière modification", // "item.edit.tabs.status.title": "Item Edit - Status", - // TODO New key - Add a translation - "item.edit.tabs.status.title": "Item Edit - Status", - + "item.edit.tabs.status.title": "Édition Item - Statut", // "item.edit.tabs.view.head": "View Item", - // TODO New key - Add a translation - "item.edit.tabs.view.head": "View Item", - + "item.edit.tabs.view.head": "Voir Item", // "item.edit.tabs.view.title": "Item Edit - View", - // TODO New key - Add a translation - "item.edit.tabs.view.title": "Item Edit - View", - - - + "item.edit.tabs.view.title": "Édition Item - View", // "item.edit.withdraw.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.withdraw.cancel": "Cancel", - + "item.edit.withdraw.cancel": "Annuler", // "item.edit.withdraw.confirm": "Withdraw", - // TODO New key - Add a translation - "item.edit.withdraw.confirm": "Withdraw", - + "item.edit.withdraw.confirm": "Retirer", // "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", - // TODO New key - Add a translation - "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", - + "item.edit.withdraw.description": "Êtes-vous certain que cet Item doit être retiré du dépôt ?", // "item.edit.withdraw.error": "An error occurred while withdrawing the item", - // TODO New key - Add a translation - "item.edit.withdraw.error": "An error occurred while withdrawing the item", - + "item.edit.withdraw.error": "Une erreur s'est produite lors du retrait de l'Item", // "item.edit.withdraw.header": "Withdraw item: {{ id }}", - // TODO New key - Add a translation - "item.edit.withdraw.header": "Withdraw item: {{ id }}", - + "item.edit.withdraw.header": "Retrait item : {{ id }}", // "item.edit.withdraw.success": "The item was withdrawn successfully", - // TODO New key - Add a translation - "item.edit.withdraw.success": "The item was withdrawn successfully", - - + "item.edit.withdraw.success": "L'Item a été retiré avec succès", // "item.page.abstract": "Abstract", - // TODO New key - Add a translation - "item.page.abstract": "Abstract", - + "item.page.abstract": "Résumé", // "item.page.author": "Authors", - // TODO New key - Add a translation - "item.page.author": "Authors", - + "item.page.author": "Auteurs", // "item.page.citation": "Citation", - // TODO New key - Add a translation "item.page.citation": "Citation", - // "item.page.collections": "Collections", - // TODO New key - Add a translation "item.page.collections": "Collections", - // "item.page.date": "Date", - // TODO New key - Add a translation "item.page.date": "Date", - // "item.page.files": "Files", - // TODO New key - Add a translation - "item.page.files": "Files", - + "item.page.files": "Fichiers", // "item.page.filesection.description": "Description:", - // TODO New key - Add a translation - "item.page.filesection.description": "Description:", - + "item.page.filesection.description": "Description :", // "item.page.filesection.download": "Download", - // TODO New key - Add a translation - "item.page.filesection.download": "Download", - + "item.page.filesection.download": "Télécharger", // "item.page.filesection.format": "Format:", - // TODO New key - Add a translation - "item.page.filesection.format": "Format:", - + "item.page.filesection.format": "Format :", // "item.page.filesection.name": "Name:", - // TODO New key - Add a translation - "item.page.filesection.name": "Name:", - + "item.page.filesection.name": "Nom :", // "item.page.filesection.size": "Size:", - // TODO New key - Add a translation - "item.page.filesection.size": "Size:", - + "item.page.filesection.size": "Taille :", // "item.page.journal.search.title": "Articles in this journal", - // TODO New key - Add a translation - "item.page.journal.search.title": "Articles in this journal", - + "item.page.journal.search.title": "Articles dans ce Périodique", // "item.page.link.full": "Full item page", - // TODO New key - Add a translation - "item.page.link.full": "Full item page", - + "item.page.link.full": "Notice complète", // "item.page.link.simple": "Simple item page", - // TODO New key - Add a translation - "item.page.link.simple": "Simple item page", - + "item.page.link.simple": "Notice simple", // "item.page.person.search.title": "Articles by this author", - // TODO New key - Add a translation - "item.page.person.search.title": "Articles by this author", - + "item.page.person.search.title": "Articles de cet auteur", // "item.page.related-items.view-more": "View more", - // TODO New key - Add a translation - "item.page.related-items.view-more": "View more", - + "item.page.related-items.view-more": "Plus de résultats", // "item.page.related-items.view-less": "View less", - // TODO New key - Add a translation - "item.page.related-items.view-less": "View less", - + "item.page.related-items.view-less": "Moins de résultats", // "item.page.subject": "Keywords", - // TODO New key - Add a translation - "item.page.subject": "Keywords", - + "item.page.subject": "Mots-clés", // "item.page.uri": "URI", - // TODO New key - Add a translation "item.page.uri": "URI", - - // "item.select.confirm": "Confirm selected", - // TODO New key - Add a translation - "item.select.confirm": "Confirm selected", - + "item.select.confirm": "Confirmer sélection", // "item.select.empty": "No items to show", - // TODO New key - Add a translation - "item.select.empty": "No items to show", - + "item.select.empty": "Aucun Item à afficher", // "item.select.table.author": "Author", - // TODO New key - Add a translation - "item.select.table.author": "Author", - + "item.select.table.author": "Auteur", // "item.select.table.collection": "Collection", - // TODO New key - Add a translation "item.select.table.collection": "Collection", - // "item.select.table.title": "Title", - // TODO New key - Add a translation - "item.select.table.title": "Title", - - + "item.select.table.title": "Titre", // "journal.listelement.badge": "Journal", - // TODO New key - Add a translation - "journal.listelement.badge": "Journal", - + "journal.listelement.badge": "Périodique", // "journal.page.description": "Description", - // TODO New key - Add a translation "journal.page.description": "Description", - // "journal.page.editor": "Editor-in-Chief", - // TODO New key - Add a translation - "journal.page.editor": "Editor-in-Chief", - + "journal.page.editor": "Rédacteur en chef", // "journal.page.issn": "ISSN", - // TODO New key - Add a translation "journal.page.issn": "ISSN", - // "journal.page.publisher": "Publisher", - // TODO New key - Add a translation - "journal.page.publisher": "Publisher", - + "journal.page.publisher": "Éditeur", // "journal.page.titleprefix": "Journal: ", - // TODO New key - Add a translation - "journal.page.titleprefix": "Journal: ", - + "journal.page.titleprefix": "Périodique : ", // "journal.search.results.head": "Journal Search Results", - // TODO New key - Add a translation - "journal.search.results.head": "Journal Search Results", - + "journal.search.results.head": "Résultats de recherche Périodique", // "journal.search.title": "DSpace Angular :: Journal Search", - // TODO New key - Add a translation - "journal.search.title": "DSpace Angular :: Journal Search", - - - + "journal.search.title": "DSpace Angular :: Recherche Périodique", // "journalissue.listelement.badge": "Journal Issue", - // TODO New key - Add a translation - "journalissue.listelement.badge": "Journal Issue", - + "journalissue.listelement.badge": "Numéro de Périodique", // "journalissue.page.description": "Description", - // TODO New key - Add a translation "journalissue.page.description": "Description", - // "journalissue.page.issuedate": "Issue Date", - // TODO New key - Add a translation - "journalissue.page.issuedate": "Issue Date", - + "journalissue.page.issuedate": "Date de publication", // "journalissue.page.journal-issn": "Journal ISSN", - // TODO New key - Add a translation - "journalissue.page.journal-issn": "Journal ISSN", - + "journalissue.page.journal-issn": "ISSN du Périodique", // "journalissue.page.journal-title": "Journal Title", - // TODO New key - Add a translation - "journalissue.page.journal-title": "Journal Title", - + "journalissue.page.journal-title": "Titre du Périodique", // "journalissue.page.keyword": "Keywords", - // TODO New key - Add a translation - "journalissue.page.keyword": "Keywords", - + "journalissue.page.keyword": "Mots-clés", // "journalissue.page.number": "Number", - // TODO New key - Add a translation - "journalissue.page.number": "Number", - + "journalissue.page.number": "Nombre", // "journalissue.page.titleprefix": "Journal Issue: ", - // TODO New key - Add a translation - "journalissue.page.titleprefix": "Journal Issue: ", - - - + "journalissue.page.titleprefix": "Numéro de Périodique : ", + // "journalvolume.listelement.badge": "Journal Volume", - // TODO New key - Add a translation - "journalvolume.listelement.badge": "Journal Volume", - + "journalvolume.listelement.badge": "Volume de Périodique", // "journalvolume.page.description": "Description", - // TODO New key - Add a translation "journalvolume.page.description": "Description", - // "journalvolume.page.issuedate": "Issue Date", - // TODO New key - Add a translation - "journalvolume.page.issuedate": "Issue Date", - + "journalvolume.page.issuedate": "Date de publication", // "journalvolume.page.titleprefix": "Journal Volume: ", - // TODO New key - Add a translation - "journalvolume.page.titleprefix": "Journal Volume: ", - + "journalvolume.page.titleprefix": "Volume de Périodique : ", // "journalvolume.page.volume": "Volume", - // TODO New key - Add a translation "journalvolume.page.volume": "Volume", - - // "loading.browse-by": "Loading items...", - // TODO New key - Add a translation - "loading.browse-by": "Loading items...", - + "loading.browse-by": "Items en cours de chargement...", // "loading.browse-by-page": "Loading page...", - // TODO New key - Add a translation - "loading.browse-by-page": "Loading page...", - + "loading.browse-by-page": "Page en cours de chargement...", // "loading.collection": "Loading collection...", - // TODO New key - Add a translation - "loading.collection": "Loading collection...", - + "loading.collection": "Collection en cours de chargement...", // "loading.collections": "Loading collections...", - // TODO New key - Add a translation - "loading.collections": "Loading collections...", - + "loading.collections": "Collections en cours de chargement...", // "loading.community": "Loading community...", - // TODO New key - Add a translation - "loading.community": "Loading community...", - + "loading.community": "Communauté en cours de chargement...", // "loading.default": "Loading...", - // TODO New key - Add a translation - "loading.default": "Loading...", - + "loading.default": "En cours de chargement...", // "loading.item": "Loading item...", - // TODO New key - Add a translation - "loading.item": "Loading item...", - + "loading.item": "Item en cours de chargement...", // "loading.items": "Loading items...", - // TODO New key - Add a translation - "loading.items": "Loading items...", - + "loading.items": "Items en cours de chargement...", // "loading.mydspace-results": "Loading items...", - // TODO New key - Add a translation - "loading.mydspace-results": "Loading items...", - + "loading.mydspace-results": "Items en cours de chargement...", // "loading.objects": "Loading...", - // TODO New key - Add a translation - "loading.objects": "Loading...", - + "loading.objects": "En cours de chargement...", // "loading.recent-submissions": "Loading recent submissions...", - // TODO New key - Add a translation - "loading.recent-submissions": "Loading recent submissions...", - + "loading.recent-submissions": "Dépôts récents en cours de chargement...", // "loading.search-results": "Loading search results...", - // TODO New key - Add a translation - "loading.search-results": "Loading search results...", - + "loading.search-results": "Résultats de recherche en cours de chargement...", // "loading.sub-collections": "Loading sub-collections...", - // TODO New key - Add a translation - "loading.sub-collections": "Loading sub-collections...", - + "loading.sub-collections": "Sous-Collections en cours de chargement...", // "loading.sub-communities": "Loading sub-communities...", - // TODO New key - Add a translation - "loading.sub-communities": "Loading sub-communities...", - + "loading.sub-communities": "Sous-Communautés en cours de chargement...", // "loading.top-level-communities": "Loading top-level communities...", - // TODO New key - Add a translation - "loading.top-level-communities": "Loading top-level communities...", - - + "loading.top-level-communities": "Communautés de 1er niveau en cours de chargement...", // "login.form.email": "Email address", - // TODO New key - Add a translation - "login.form.email": "Email address", - + "login.form.email": "Adresse e-mail", // "login.form.forgot-password": "Have you forgotten your password?", - // TODO New key - Add a translation - "login.form.forgot-password": "Have you forgotten your password?", - + "login.form.forgot-password": "Mot de passe oublié ?", // "login.form.header": "Please log in to DSpace", - // TODO New key - Add a translation - "login.form.header": "Please log in to DSpace", - + "login.form.header": "Se connecter sur DSpace", // "login.form.new-user": "New user? Click here to register.", - // TODO New key - Add a translation - "login.form.new-user": "New user? Click here to register.", - + "login.form.new-user": "Pas encore de compte ? Cliquez ici pour vous enregistrer.", // "login.form.password": "Password", - // TODO New key - Add a translation - "login.form.password": "Password", - + "login.form.password": "Mot de passe", // "login.form.submit": "Log in", - // TODO New key - Add a translation - "login.form.submit": "Log in", - + "login.form.submit": "Se connecter", // "login.title": "Login", - // TODO New key - Add a translation - "login.title": "Login", - - + "login.title": "Connection", // "logout.form.header": "Log out from DSpace", - // TODO New key - Add a translation - "logout.form.header": "Log out from DSpace", - + "logout.form.header": "Se déconnecter de DSpace", // "logout.form.submit": "Log out", - // TODO New key - Add a translation - "logout.form.submit": "Log out", - + "logout.form.submit": "Se déconnecter", // "logout.title": "Logout", - // TODO New key - Add a translation - "logout.title": "Logout", - - + "logout.title": "Déconnection", // "menu.header.admin": "Admin", - // TODO New key - Add a translation "menu.header.admin": "Admin", - // "menu.header.image.logo": "Repository logo", - // TODO New key - Add a translation - "menu.header.image.logo": "Repository logo", - - + "menu.header.image.logo": "Logo du site", // "menu.section.access_control": "Access Control", - // TODO New key - Add a translation - "menu.section.access_control": "Access Control", - + "menu.section.access_control": "Contrôle d'accès", // "menu.section.access_control_authorizations": "Authorizations", - // TODO New key - Add a translation - "menu.section.access_control_authorizations": "Authorizations", - + "menu.section.access_control_authorizations": "Autorisations", // "menu.section.access_control_groups": "Groups", - // TODO New key - Add a translation - "menu.section.access_control_groups": "Groups", - + "menu.section.access_control_groups": "Groupes d'utilisateurs", // "menu.section.access_control_people": "People", - // TODO New key - Add a translation - "menu.section.access_control_people": "People", - - - + "menu.section.access_control_people": "Utilisateurs", // "menu.section.browse_community": "This Community", - // TODO New key - Add a translation - "menu.section.browse_community": "This Community", - + "menu.section.browse_community": "Cette Communauté", // "menu.section.browse_community_by_author": "By Author", - // TODO New key - Add a translation - "menu.section.browse_community_by_author": "By Author", - + "menu.section.browse_community_by_author": "Auteur", // "menu.section.browse_community_by_issue_date": "By Issue Date", - // TODO New key - Add a translation - "menu.section.browse_community_by_issue_date": "By Issue Date", - + "menu.section.browse_community_by_issue_date": "Date de publication", // "menu.section.browse_community_by_title": "By Title", - // TODO New key - Add a translation - "menu.section.browse_community_by_title": "By Title", - + "menu.section.browse_community_by_title": "Titre", // "menu.section.browse_global": "All of DSpace", - // TODO New key - Add a translation - "menu.section.browse_global": "All of DSpace", - + "menu.section.browse_global": "Tout DSpace", // "menu.section.browse_global_by_author": "By Author", - // TODO New key - Add a translation - "menu.section.browse_global_by_author": "By Author", - + "menu.section.browse_global_by_author": "Auteur", // "menu.section.browse_global_by_dateissued": "By Issue Date", - // TODO New key - Add a translation - "menu.section.browse_global_by_dateissued": "By Issue Date", - + "menu.section.browse_global_by_dateissued": "Date de publication", // "menu.section.browse_global_by_subject": "By Subject", - // TODO New key - Add a translation - "menu.section.browse_global_by_subject": "By Subject", - + "menu.section.browse_global_by_subject": "Sujet", // "menu.section.browse_global_by_title": "By Title", - // TODO New key - Add a translation - "menu.section.browse_global_by_title": "By Title", - + "menu.section.browse_global_by_title": "Titre", // "menu.section.browse_global_communities_and_collections": "Communities & Collections", - // TODO New key - Add a translation - "menu.section.browse_global_communities_and_collections": "Communities & Collections", - - + "menu.section.browse_global_communities_and_collections": "Communautés & Collections", // "menu.section.control_panel": "Control Panel", - // TODO New key - Add a translation - "menu.section.control_panel": "Control Panel", - + "menu.section.control_panel": "Panneau de configuration", // "menu.section.curation_task": "Curation Task", - // TODO New key - Add a translation - "menu.section.curation_task": "Curation Task", - - + "menu.section.curation_task": "Tâches de Curation", // "menu.section.edit": "Edit", - // TODO New key - Add a translation - "menu.section.edit": "Edit", - + "menu.section.edit": "Éditer", // "menu.section.edit_collection": "Collection", - // TODO New key - Add a translation "menu.section.edit_collection": "Collection", - // "menu.section.edit_community": "Community", - // TODO New key - Add a translation - "menu.section.edit_community": "Community", - + "menu.section.edit_community": "Communauté", // "menu.section.edit_item": "Item", - // TODO New key - Add a translation "menu.section.edit_item": "Item", - - // "menu.section.export": "Export", - // TODO New key - Add a translation - "menu.section.export": "Export", - + "menu.section.export": "Exporter", // "menu.section.export_collection": "Collection", - // TODO New key - Add a translation "menu.section.export_collection": "Collection", - // "menu.section.export_community": "Community", - // TODO New key - Add a translation - "menu.section.export_community": "Community", - + "menu.section.export_community": "Communauté", // "menu.section.export_item": "Item", - // TODO New key - Add a translation "menu.section.export_item": "Item", - // "menu.section.export_metadata": "Metadata", - // TODO New key - Add a translation - "menu.section.export_metadata": "Metadata", - - + "menu.section.export_metadata": "Métadonnées", // "menu.section.find": "Find", - // TODO New key - Add a translation - "menu.section.find": "Find", - + "menu.section.find": "Rechercher", // "menu.section.find_items": "Items", - // TODO New key - Add a translation "menu.section.find_items": "Items", - // "menu.section.find_private_items": "Private Items", - // TODO New key - Add a translation - "menu.section.find_private_items": "Private Items", - + "menu.section.find_private_items": "Items Privés", // "menu.section.find_withdrawn_items": "Withdrawn Items", - // TODO New key - Add a translation - "menu.section.find_withdrawn_items": "Withdrawn Items", - - - + "menu.section.find_withdrawn_items": "Items Retirés", + + // These are the hints when hovering over the icons of the sidebar menu // "menu.section.icon.access_control": "Access Control menu section", - // TODO New key - Add a translation - "menu.section.icon.access_control": "Access Control menu section", - + "menu.section.icon.access_control": "Section du menu relative au contrôle d'accès", // "menu.section.icon.control_panel": "Control Panel menu section", - // TODO New key - Add a translation - "menu.section.icon.control_panel": "Control Panel menu section", - + "menu.section.icon.control_panel": "Section du menu relative au panneau de configuration", // "menu.section.icon.curation_task": "Curation Task menu section", - // TODO New key - Add a translation - "menu.section.icon.curation_task": "Curation Task menu section", - + "menu.section.icon.curation_task": "Section du menu relative aux tâches de Curation", // "menu.section.icon.edit": "Edit menu section", - // TODO New key - Add a translation - "menu.section.icon.edit": "Edit menu section", - + "menu.section.icon.edit": "Section du menu relative à l'édition", // "menu.section.icon.export": "Export menu section", - // TODO New key - Add a translation - "menu.section.icon.export": "Export menu section", - + "menu.section.icon.export": "Section du menu relative à l'export", // "menu.section.icon.find": "Find menu section", - // TODO New key - Add a translation - "menu.section.icon.find": "Find menu section", - + "menu.section.icon.find": "Section du menu relative à la recherche", // "menu.section.icon.import": "Import menu section", - // TODO New key - Add a translation - "menu.section.icon.import": "Import menu section", - + "menu.section.icon.import": "Section du menu relative à l'import", // "menu.section.icon.new": "New menu section", - // TODO New key - Add a translation - "menu.section.icon.new": "New menu section", - + "menu.section.icon.new": "Section du menu relative à l'ajout d'éléments", // "menu.section.icon.pin": "Pin sidebar", - // TODO New key - Add a translation - "menu.section.icon.pin": "Pin sidebar", - + "menu.section.icon.pin": "Epingler menu latéral", // "menu.section.icon.registries": "Registries menu section", - // TODO New key - Add a translation - "menu.section.icon.registries": "Registries menu section", - + "menu.section.icon.registries": "Section du menu relative aux Registres", // "menu.section.icon.statistics_task": "Statistics Task menu section", - // TODO New key - Add a translation - "menu.section.icon.statistics_task": "Statistics Task menu section", - + "menu.section.icon.statistics_task": "Section du menu relative aux tâches Statistiques", // "menu.section.icon.unpin": "Unpin sidebar", - // TODO New key - Add a translation - "menu.section.icon.unpin": "Unpin sidebar", - - + "menu.section.icon.unpin": "Libérer menu latéral", // "menu.section.import": "Import", - // TODO New key - Add a translation - "menu.section.import": "Import", - + "menu.section.import": "Importer", // "menu.section.import_batch": "Batch Import (ZIP)", // TODO New key - Add a translation - "menu.section.import_batch": "Batch Import (ZIP)", - + "menu.section.import_batch": "Import par lot (ZIP)", // "menu.section.import_metadata": "Metadata", - // TODO New key - Add a translation - "menu.section.import_metadata": "Metadata", - - + "menu.section.import_metadata": "Import de métadonnées", // "menu.section.new": "New", - // TODO New key - Add a translation - "menu.section.new": "New", - + "menu.section.new": "Créer", // "menu.section.new_collection": "Collection", - // TODO New key - Add a translation "menu.section.new_collection": "Collection", - // "menu.section.new_community": "Community", - // TODO New key - Add a translation - "menu.section.new_community": "Community", - + "menu.section.new_community": "Communauté", // "menu.section.new_item": "Item", - // TODO New key - Add a translation "menu.section.new_item": "Item", - // "menu.section.new_item_version": "Item Version", - // TODO New key - Add a translation - "menu.section.new_item_version": "Item Version", - - - + "menu.section.new_item_version": "Version", // "menu.section.pin": "Pin sidebar", - // TODO New key - Add a translation - "menu.section.pin": "Pin sidebar", - + "menu.section.pin": "Epingler menu latéral", // "menu.section.unpin": "Unpin sidebar", - // TODO New key - Add a translation - "menu.section.unpin": "Unpin sidebar", - - + "menu.section.unpin": "Libérer menu latéral", // "menu.section.registries": "Registries", - // TODO New key - Add a translation - "menu.section.registries": "Registries", - + "menu.section.registries": "Registres", // "menu.section.registries_format": "Format", - // TODO New key - Add a translation "menu.section.registries_format": "Format", - // "menu.section.registries_metadata": "Metadata", - // TODO New key - Add a translation - "menu.section.registries_metadata": "Metadata", - - + "menu.section.registries_metadata": "Métadonnées", // "menu.section.statistics": "Statistics", - // TODO New key - Add a translation - "menu.section.statistics": "Statistics", - + "menu.section.statistics": "Statistiques", // "menu.section.statistics_task": "Statistics Task", - // TODO New key - Add a translation - "menu.section.statistics_task": "Statistics Task", - - + "menu.section.statistics_task": "Tâche statistiques", // "menu.section.toggle.access_control": "Toggle Access Control section", - // TODO New key - Add a translation - "menu.section.toggle.access_control": "Toggle Access Control section", - + "menu.section.toggle.access_control": "Ouvrir/Fermer section Contrôle d'accès", // "menu.section.toggle.control_panel": "Toggle Control Panel section", - // TODO New key - Add a translation - "menu.section.toggle.control_panel": "Toggle Control Panel section", - + "menu.section.toggle.control_panel": "Ouvrir/Fermer section Panneau de configuration", // "menu.section.toggle.curation_task": "Toggle Curation Task section", - // TODO New key - Add a translation - "menu.section.toggle.curation_task": "Toggle Curation Task section", - + "menu.section.toggle.curation_task": "Ouvrir/Fermer section Tâche de Curation", // "menu.section.toggle.edit": "Toggle Edit section", - // TODO New key - Add a translation - "menu.section.toggle.edit": "Toggle Edit section", - + "menu.section.toggle.edit": "Ouvrir/Fermer section Éditer", // "menu.section.toggle.export": "Toggle Export section", - // TODO New key - Add a translation - "menu.section.toggle.export": "Toggle Export section", - + "menu.section.toggle.export": "Ouvrir/Fermer section Exporter", // "menu.section.toggle.find": "Toggle Find section", - // TODO New key - Add a translation - "menu.section.toggle.find": "Toggle Find section", - + "menu.section.toggle.find": "Ouvrir/Fermer section Rechercher", // "menu.section.toggle.import": "Toggle Import section", - // TODO New key - Add a translation - "menu.section.toggle.import": "Toggle Import section", - + "menu.section.toggle.import": "Ouvrir/Fermer section Importer", // "menu.section.toggle.new": "Toggle New section", - // TODO New key - Add a translation - "menu.section.toggle.new": "Toggle New section", - + "menu.section.toggle.new": "Ouvrir/Fermer section Créer", // "menu.section.toggle.registries": "Toggle Registries section", - // TODO New key - Add a translation - "menu.section.toggle.registries": "Toggle Registries section", - + "menu.section.toggle.registries": "Ouvrir/Fermer section Registres", // "menu.section.toggle.statistics_task": "Toggle Statistics Task section", - // TODO New key - Add a translation - "menu.section.toggle.statistics_task": "Toggle Statistics Task section", - - + "menu.section.toggle.statistics_task": "Ouvrir/Fermer section Tâche statistiques", // "mydspace.description": "", - // TODO New key - Add a translation "mydspace.description": "", - // "mydspace.general.text-here": "HERE", - // TODO New key - Add a translation - "mydspace.general.text-here": "HERE", - + "mydspace.general.text-here": "ICI", // "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", - // TODO New key - Add a translation - "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", - + "mydspace.messages.controller-help": "Sélectionner cette option pour envoyer un message au déposant.", // "mydspace.messages.description-placeholder": "Insert your message here...", - // TODO New key - Add a translation - "mydspace.messages.description-placeholder": "Insert your message here...", - + "mydspace.messages.description-placeholder": "Insérer votre message ici...", // "mydspace.messages.hide-msg": "Hide message", - // TODO New key - Add a translation - "mydspace.messages.hide-msg": "Hide message", - + "mydspace.messages.hide-msg": "Masquer message", // "mydspace.messages.mark-as-read": "Mark as read", - // TODO New key - Add a translation - "mydspace.messages.mark-as-read": "Mark as read", - + "mydspace.messages.mark-as-read": "Marquer comme lu", // "mydspace.messages.mark-as-unread": "Mark as unread", - // TODO New key - Add a translation - "mydspace.messages.mark-as-unread": "Mark as unread", - + "mydspace.messages.mark-as-unread": "Marquer comme non lu", // "mydspace.messages.no-content": "No content.", - // TODO New key - Add a translation - "mydspace.messages.no-content": "No content.", - + "mydspace.messages.no-content": "Aucun contenu.", // "mydspace.messages.no-messages": "No messages yet.", - // TODO New key - Add a translation - "mydspace.messages.no-messages": "No messages yet.", - + "mydspace.messages.no-messages": "Pas encore de messages.", // "mydspace.messages.send-btn": "Send", - // TODO New key - Add a translation - "mydspace.messages.send-btn": "Send", - + "mydspace.messages.send-btn": "Envoyer", // "mydspace.messages.show-msg": "Show message", - // TODO New key - Add a translation - "mydspace.messages.show-msg": "Show message", - + "mydspace.messages.show-msg": "Afficher message", // "mydspace.messages.subject-placeholder": "Subject...", - // TODO New key - Add a translation - "mydspace.messages.subject-placeholder": "Subject...", - + "mydspace.messages.subject-placeholder": "Objet...", // "mydspace.messages.submitter-help": "Select this option to send a message to controller.", - // TODO New key - Add a translation - "mydspace.messages.submitter-help": "Select this option to send a message to controller.", - + "mydspace.messages.submitter-help": "Sélectionner cette option pour envoyer un message au vérificateur.", // "mydspace.messages.title": "Messages", - // TODO New key - Add a translation "mydspace.messages.title": "Messages", - // "mydspace.messages.to": "To", - // TODO New key - Add a translation - "mydspace.messages.to": "To", - + "mydspace.messages.to": "À", // "mydspace.new-submission": "New submission", - // TODO New key - Add a translation - "mydspace.new-submission": "New submission", - + "mydspace.new-submission": "Nouveau dépôt", // "mydspace.results.head": "Your submissions", - // TODO New key - Add a translation - "mydspace.results.head": "Your submissions", - + "mydspace.results.head": "Vos dépôts", // "mydspace.results.no-abstract": "No Abstract", - // TODO New key - Add a translation - "mydspace.results.no-abstract": "No Abstract", - + "mydspace.results.no-abstract": "Aucun résumé", // "mydspace.results.no-authors": "No Authors", - // TODO New key - Add a translation - "mydspace.results.no-authors": "No Authors", - + "mydspace.results.no-authors": "Aucun auteur", // "mydspace.results.no-collections": "No Collections", - // TODO New key - Add a translation - "mydspace.results.no-collections": "No Collections", - + "mydspace.results.no-collections": "Aucune Collection", // "mydspace.results.no-date": "No Date", - // TODO New key - Add a translation - "mydspace.results.no-date": "No Date", - + "mydspace.results.no-date": "Aucune date", // "mydspace.results.no-files": "No Files", - // TODO New key - Add a translation - "mydspace.results.no-files": "No Files", - + "mydspace.results.no-files": "Aucun fichier", // "mydspace.results.no-results": "There were no items to show", - // TODO New key - Add a translation - "mydspace.results.no-results": "There were no items to show", - + "mydspace.results.no-results": "Aucun Item à afficher", // "mydspace.results.no-title": "No title", - // TODO New key - Add a translation - "mydspace.results.no-title": "No title", - + "mydspace.results.no-title": "Aucun titre", // "mydspace.results.no-uri": "No Uri", - // TODO New key - Add a translation - "mydspace.results.no-uri": "No Uri", - + "mydspace.results.no-uri": "Aucune URL", // "mydspace.show.workflow": "All tasks", - // TODO New key - Add a translation - "mydspace.show.workflow": "All tasks", - + "mydspace.show.workflow": "Tâches de validation", // "mydspace.show.workspace": "Your Submissions", - // TODO New key - Add a translation - "mydspace.show.workspace": "Your Submissions", - + "mydspace.show.workspace": "Vos dépôts", // "mydspace.status.archived": "Archived", - // TODO New key - Add a translation - "mydspace.status.archived": "Archived", - + "mydspace.status.archived": "Archivés", // "mydspace.status.validation": "Validation", - // TODO New key - Add a translation - "mydspace.status.validation": "Validation", - + "mydspace.status.validation": "En cours de validation", // "mydspace.status.waiting-for-controller": "Waiting for controller", - // TODO New key - Add a translation - "mydspace.status.waiting-for-controller": "Waiting for controller", - + "mydspace.status.waiting-for-controller": "En attente d'assignation", // "mydspace.status.workflow": "Workflow", - // TODO New key - Add a translation "mydspace.status.workflow": "Workflow", - // "mydspace.status.workspace": "Workspace", - // TODO New key - Add a translation "mydspace.status.workspace": "Workspace", - // "mydspace.title": "MyDSpace", - // TODO New key - Add a translation "mydspace.title": "MyDSpace", - // "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", - // TODO New key - Add a translation - "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", - + "mydspace.upload.upload-failed": "Erreur à la création du Workspace. Veuillez vérifier le contenu téléchargé avant de réessayer.", // "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", - // TODO New key - Add a translation - "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", - + "mydspace.upload.upload-multiple-successful": "{{qty}} nouveaux Items créés dans le Workspace.", // "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", - // TODO New key - Add a translation - "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", - + "mydspace.upload.upload-successful": "Nouvel item créé dans le Workspace. Cliquer {{here}} pour l'éditer.", // "mydspace.view-btn": "View", - // TODO New key - Add a translation - "mydspace.view-btn": "View", - - + "mydspace.view-btn": "Afficher", // "nav.browse.header": "All of DSpace", - // TODO New key - Add a translation - "nav.browse.header": "All of DSpace", - + "nav.browse.header": "Tout Dspace", // "nav.community-browse.header": "By Community", - // TODO New key - Add a translation - "nav.community-browse.header": "By Community", - + "nav.community-browse.header": "Par Communauté", // "nav.language": "Language switch", - // TODO New key - Add a translation - "nav.language": "Language switch", - + "nav.language": "Sélecteur de langue", // "nav.login": "Log In", - // TODO New key - Add a translation - "nav.login": "Log In", - + "nav.login": "Se connecter", // "nav.logout": "Log Out", - // TODO New key - Add a translation - "nav.logout": "Log Out", - + "nav.logout": "Se déconnecter", // "nav.mydspace": "MyDSpace", - // TODO New key - Add a translation "nav.mydspace": "MyDSpace", - // "nav.search": "Search", - // TODO New key - Add a translation - "nav.search": "Search", - + "nav.search": "Rechercher", // "nav.statistics.header": "Statistics", - // TODO New key - Add a translation - "nav.statistics.header": "Statistics", - - + "nav.statistics.header": "Statistiques", // "orgunit.listelement.badge": "Organizational Unit", - // TODO New key - Add a translation - "orgunit.listelement.badge": "Organizational Unit", - + "orgunit.listelement.badge": "Structure Organisationnelle", // "orgunit.page.city": "City", - // TODO New key - Add a translation - "orgunit.page.city": "City", - + "orgunit.page.city": "Ville", // "orgunit.page.country": "Country", - // TODO New key - Add a translation - "orgunit.page.country": "Country", - + "orgunit.page.country": "Pays", // "orgunit.page.dateestablished": "Date established", - // TODO New key - Add a translation - "orgunit.page.dateestablished": "Date established", - + "orgunit.page.dateestablished": "Date de fondation", // "orgunit.page.description": "Description", - // TODO New key - Add a translation "orgunit.page.description": "Description", - // "orgunit.page.id": "ID", - // TODO New key - Add a translation "orgunit.page.id": "ID", - // "orgunit.page.titleprefix": "Organizational Unit: ", - // TODO New key - Add a translation - "orgunit.page.titleprefix": "Organizational Unit: ", - - + "orgunit.page.titleprefix": "Structure Organisationnelle : ", // "pagination.results-per-page": "Results Per Page", - // TODO New key - Add a translation - "pagination.results-per-page": "Results Per Page", - + "pagination.results-per-page": "Résultats par page", // "pagination.showing.detail": "{{ range }} of {{ total }}", - // TODO New key - Add a translation - "pagination.showing.detail": "{{ range }} of {{ total }}", - + "pagination.showing.detail": "{{ range }} sur {{ total }}", // "pagination.showing.label": "Now showing ", - // TODO New key - Add a translation - "pagination.showing.label": "Now showing ", - + "pagination.showing.label": "Voici les éléments ", // "pagination.sort-direction": "Sort Options", - // TODO New key - Add a translation - "pagination.sort-direction": "Sort Options", - - + "pagination.sort-direction": "Options de tri", // "person.listelement.badge": "Person", - // TODO New key - Add a translation - "person.listelement.badge": "Person", - + "person.listelement.badge": "Personne", // "person.page.birthdate": "Birth Date", - // TODO New key - Add a translation - "person.page.birthdate": "Birth Date", - + "person.page.birthdate": "Date de naissance", // "person.page.email": "Email Address", - // TODO New key - Add a translation - "person.page.email": "Email Address", - + "person.page.email": "Adresse mail", // "person.page.firstname": "First Name", - // TODO New key - Add a translation - "person.page.firstname": "First Name", - + "person.page.firstname": "Prénom", // "person.page.jobtitle": "Job Title", - // TODO New key - Add a translation - "person.page.jobtitle": "Job Title", - + "person.page.jobtitle": "Fonction", // "person.page.lastname": "Last Name", - // TODO New key - Add a translation - "person.page.lastname": "Last Name", - + "person.page.lastname": "Nom de famille", // "person.page.link.full": "Show all metadata", - // TODO New key - Add a translation - "person.page.link.full": "Show all metadata", - + "person.page.link.full": "Afficher toutes les métadonnées", // "person.page.orcid": "ORCID", - // TODO New key - Add a translation "person.page.orcid": "ORCID", - // "person.page.staffid": "Staff ID", - // TODO New key - Add a translation "person.page.staffid": "Staff ID", - // "person.page.titleprefix": "Person: ", - // TODO New key - Add a translation - "person.page.titleprefix": "Person: ", - + "person.page.titleprefix": "Personne : ", // "person.search.results.head": "Person Search Results", - // TODO New key - Add a translation - "person.search.results.head": "Person Search Results", - + "person.search.results.head": "Résultats de recherche Personne", // "person.search.title": "DSpace Angular :: Person Search", - // TODO New key - Add a translation - "person.search.title": "DSpace Angular :: Person Search", - - + "person.search.title": "DSpace Angular :: Recherche Personne", // "project.listelement.badge": "Research Project", - // TODO New key - Add a translation - "project.listelement.badge": "Research Project", - + "project.listelement.badge": "Projet de recherche", // "project.page.contributor": "Contributors", - // TODO New key - Add a translation - "project.page.contributor": "Contributors", - + "project.page.contributor": "Contributeurs", // "project.page.description": "Description", - // TODO New key - Add a translation "project.page.description": "Description", - // "project.page.expectedcompletion": "Expected Completion", - // TODO New key - Add a translation - "project.page.expectedcompletion": "Expected Completion", - + "project.page.expectedcompletion": "Échéance prévue", // "project.page.funder": "Funders", - // TODO New key - Add a translation - "project.page.funder": "Funders", - + "project.page.funder": "Bailleurs de fonds", // "project.page.id": "ID", - // TODO New key - Add a translation "project.page.id": "ID", - // "project.page.keyword": "Keywords", - // TODO New key - Add a translation - "project.page.keyword": "Keywords", - + "project.page.keyword": "Mots-clés", // "project.page.status": "Status", - // TODO New key - Add a translation - "project.page.status": "Status", - + "project.page.status": "Statut", // "project.page.titleprefix": "Research Project: ", - // TODO New key - Add a translation - "project.page.titleprefix": "Research Project: ", - - + "project.page.titleprefix": "Projet de recherche : ", // "publication.listelement.badge": "Publication", - // TODO New key - Add a translation "publication.listelement.badge": "Publication", - // "publication.page.description": "Description", - // TODO New key - Add a translation "publication.page.description": "Description", - // "publication.page.journal-issn": "Journal ISSN", - // TODO New key - Add a translation - "publication.page.journal-issn": "Journal ISSN", - + "publication.page.journal-issn": "ISSN du Périodique", // "publication.page.journal-title": "Journal Title", - // TODO New key - Add a translation - "publication.page.journal-title": "Journal Title", - + "publication.page.journal-title": "Titre du Périodique", // "publication.page.publisher": "Publisher", - // TODO New key - Add a translation - "publication.page.publisher": "Publisher", - + "publication.page.publisher": "Éditeur", // "publication.page.titleprefix": "Publication: ", - // TODO New key - Add a translation - "publication.page.titleprefix": "Publication: ", - + "publication.page.titleprefix": "Publication : ", // "publication.page.volume-title": "Volume Title", - // TODO New key - Add a translation - "publication.page.volume-title": "Volume Title", - + "publication.page.volume-title": "Titre du volume", // "publication.search.results.head": "Publication Search Results", - // TODO New key - Add a translation - "publication.search.results.head": "Publication Search Results", - + "publication.search.results.head": "Résultats de recherche Publication", // "publication.search.title": "DSpace Angular :: Publication Search", - // TODO New key - Add a translation - "publication.search.title": "DSpace Angular :: Publication Search", - - + "publication.search.title": "DSpace Angular :: Recherche Publication", // "relationships.isAuthorOf": "Authors", - // TODO New key - Add a translation - "relationships.isAuthorOf": "Authors", - + "relationships.isAuthorOf": "Auteurs", // "relationships.isIssueOf": "Journal Issues", - // TODO New key - Add a translation - "relationships.isIssueOf": "Journal Issues", - + "relationships.isIssueOf": "Numéros de Périodique", // "relationships.isJournalIssueOf": "Journal Issue", - // TODO New key - Add a translation - "relationships.isJournalIssueOf": "Journal Issue", - + "relationships.isJournalIssueOf": "Numéro de Périodique", // "relationships.isJournalOf": "Journals", - // TODO New key - Add a translation - "relationships.isJournalOf": "Journals", - + "relationships.isJournalOf": "Périodiques", // "relationships.isOrgUnitOf": "Organizational Units", - // TODO New key - Add a translation - "relationships.isOrgUnitOf": "Organizational Units", - + "relationships.isOrgUnitOf": "Structure organisationnelle", // "relationships.isPersonOf": "Authors", - // TODO New key - Add a translation - "relationships.isPersonOf": "Authors", - + "relationships.isPersonOf": "Auteurs", // "relationships.isProjectOf": "Research Projects", - // TODO New key - Add a translation - "relationships.isProjectOf": "Research Projects", - + "relationships.isProjectOf": "Projets de recherche", // "relationships.isPublicationOf": "Publications", - // TODO New key - Add a translation "relationships.isPublicationOf": "Publications", - // "relationships.isPublicationOfJournalIssue": "Articles", - // TODO New key - Add a translation "relationships.isPublicationOfJournalIssue": "Articles", - // "relationships.isSingleJournalOf": "Journal", - // TODO New key - Add a translation - "relationships.isSingleJournalOf": "Journal", - + "relationships.isSingleJournalOf": "Périodique", // "relationships.isSingleVolumeOf": "Journal Volume", - // TODO New key - Add a translation - "relationships.isSingleVolumeOf": "Journal Volume", - + "relationships.isSingleVolumeOf": "Volume de Périodique", // "relationships.isVolumeOf": "Journal Volumes", - // TODO New key - Add a translation - "relationships.isVolumeOf": "Journal Volumes", - - + "relationships.isVolumeOf": "Volumes de Périodique", // "search.description": "", - // TODO New key - Add a translation "search.description": "", - // "search.switch-configuration.title": "Show", - // TODO New key - Add a translation - "search.switch-configuration.title": "Show", - + "search.switch-configuration.title": "Afficher", // "search.title": "DSpace Angular :: Search", - // TODO New key - Add a translation - "search.title": "DSpace Angular :: Search", - - - + "search.title": "DSpace Angular :: Recherche", // "search.filters.applied.f.author": "Author", - // TODO New key - Add a translation - "search.filters.applied.f.author": "Author", - - // "search.filters.applied.f.dateIssued.max": "End date", - // TODO New key - Add a translation - "search.filters.applied.f.dateIssued.max": "End date", - + "search.filters.applied.f.author": "Auteur", // "search.filters.applied.f.dateIssued.min": "Start date", - // TODO New key - Add a translation - "search.filters.applied.f.dateIssued.min": "Start date", - + "search.filters.applied.f.dateIssued.min": "Depuis", + // "search.filters.applied.f.dateIssued.max": "End date", + "search.filters.applied.f.dateIssued.max": "Jusque", // "search.filters.applied.f.dateSubmitted": "Date submitted", - // TODO New key - Add a translation - "search.filters.applied.f.dateSubmitted": "Date submitted", - + "search.filters.applied.f.dateSubmitted": "Date de dépôt", // "search.filters.applied.f.entityType": "Item Type", - // TODO New key - Add a translation - "search.filters.applied.f.entityType": "Item Type", - + "search.filters.applied.f.entityType": "Type d'Item", // "search.filters.applied.f.has_content_in_original_bundle": "Has files", - // TODO New key - Add a translation - "search.filters.applied.f.has_content_in_original_bundle": "Has files", - + "search.filters.applied.f.has_content_in_original_bundle": "Fichier(s) présent(s)", // "search.filters.applied.f.itemtype": "Type", - // TODO New key - Add a translation "search.filters.applied.f.itemtype": "Type", - // "search.filters.applied.f.namedresourcetype": "Status", - // TODO New key - Add a translation - "search.filters.applied.f.namedresourcetype": "Status", - + "search.filters.applied.f.namedresourcetype": "Statut", // "search.filters.applied.f.subject": "Subject", - // TODO New key - Add a translation - "search.filters.applied.f.subject": "Subject", - + "search.filters.applied.f.subject": "Sujet", // "search.filters.applied.f.submitter": "Submitter", - // TODO New key - Add a translation - "search.filters.applied.f.submitter": "Submitter", - - - + "search.filters.applied.f.submitter": "Déposant", // "search.filters.filter.author.head": "Author", - // TODO New key - Add a translation - "search.filters.filter.author.head": "Author", - + "search.filters.filter.author.head": "Auteur", // "search.filters.filter.author.placeholder": "Author name", - // TODO New key - Add a translation - "search.filters.filter.author.placeholder": "Author name", - + "search.filters.filter.author.placeholder": "Nom de l'auteur", // "search.filters.filter.birthDate.head": "Birth Date", - // TODO New key - Add a translation - "search.filters.filter.birthDate.head": "Birth Date", - + "search.filters.filter.birthDate.head": "Date de naissance", // "search.filters.filter.birthDate.placeholder": "Birth Date", - // TODO New key - Add a translation - "search.filters.filter.birthDate.placeholder": "Birth Date", - + "search.filters.filter.birthDate.placeholder": "Date de naissance", // "search.filters.filter.creativeDatePublished.head": "Date Published", - // TODO New key - Add a translation - "search.filters.filter.creativeDatePublished.head": "Date Published", - + "search.filters.filter.creativeDatePublished.head": "Date de publication", // "search.filters.filter.creativeDatePublished.placeholder": "Date Published", - // TODO New key - Add a translation - "search.filters.filter.creativeDatePublished.placeholder": "Date Published", - + "search.filters.filter.creativeDatePublished.placeholder": "Date de publication", // "search.filters.filter.creativeWorkEditor.head": "Editor", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkEditor.head": "Editor", - + "search.filters.filter.creativeWorkEditor.head": "Rédacteur en chef", // "search.filters.filter.creativeWorkEditor.placeholder": "Editor", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkEditor.placeholder": "Editor", - + "search.filters.filter.creativeWorkEditor.placeholder": "Rédacteur en chef", // "search.filters.filter.creativeWorkKeywords.head": "Subject", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkKeywords.head": "Subject", - + "search.filters.filter.creativeWorkKeywords.head": "Sujet", // "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", - + "search.filters.filter.creativeWorkKeywords.placeholder": "Sujet", // "search.filters.filter.creativeWorkPublisher.head": "Publisher", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkPublisher.head": "Publisher", - + "search.filters.filter.creativeWorkPublisher.head": "Éditeur", // "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", - + "search.filters.filter.creativeWorkPublisher.placeholder": "Éditeur", // "search.filters.filter.dateIssued.head": "Date", - // TODO New key - Add a translation "search.filters.filter.dateIssued.head": "Date", - // "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", - // TODO New key - Add a translation - "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", - + "search.filters.filter.dateIssued.max.placeholder": "Depuis", // "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", - // TODO New key - Add a translation - "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", - + "search.filters.filter.dateIssued.min.placeholder": "Jusque", // "search.filters.filter.dateSubmitted.head": "Date submitted", - // TODO New key - Add a translation - "search.filters.filter.dateSubmitted.head": "Date submitted", - + "search.filters.filter.dateSubmitted.head": "Date de dépôt", // "search.filters.filter.dateSubmitted.placeholder": "Date submitted", - // TODO New key - Add a translation - "search.filters.filter.dateSubmitted.placeholder": "Date submitted", - + "search.filters.filter.dateSubmitted.placeholder": "Date de dépôt", // "search.filters.filter.entityType.head": "Item Type", - // TODO New key - Add a translation - "search.filters.filter.entityType.head": "Item Type", - + "search.filters.filter.entityType.head": "Type d'Item", // "search.filters.filter.entityType.placeholder": "Item Type", - // TODO New key - Add a translation - "search.filters.filter.entityType.placeholder": "Item Type", - + "search.filters.filter.entityType.placeholder": "Type d'Item", // "search.filters.filter.has_content_in_original_bundle.head": "Has files", - // TODO New key - Add a translation - "search.filters.filter.has_content_in_original_bundle.head": "Has files", - + "search.filters.filter.has_content_in_original_bundle.head": "Fichier(s) présent(s)", // "search.filters.filter.itemtype.head": "Type", - // TODO New key - Add a translation "search.filters.filter.itemtype.head": "Type", - // "search.filters.filter.itemtype.placeholder": "Type", - // TODO New key - Add a translation "search.filters.filter.itemtype.placeholder": "Type", - // "search.filters.filter.jobTitle.head": "Job Title", - // TODO New key - Add a translation - "search.filters.filter.jobTitle.head": "Job Title", - + "search.filters.filter.jobTitle.head": "Fonction", // "search.filters.filter.jobTitle.placeholder": "Job Title", - // TODO New key - Add a translation - "search.filters.filter.jobTitle.placeholder": "Job Title", - + "search.filters.filter.jobTitle.placeholder": "Fonction", // "search.filters.filter.knowsLanguage.head": "Known language", - // TODO New key - Add a translation - "search.filters.filter.knowsLanguage.head": "Known language", - + "search.filters.filter.knowsLanguage.head": "Langue(s)", // "search.filters.filter.knowsLanguage.placeholder": "Known language", - // TODO New key - Add a translation - "search.filters.filter.knowsLanguage.placeholder": "Known language", - + "search.filters.filter.knowsLanguage.placeholder": "Langue(s)", // "search.filters.filter.namedresourcetype.head": "Status", - // TODO New key - Add a translation - "search.filters.filter.namedresourcetype.head": "Status", - + "search.filters.filter.namedresourcetype.head": "Statut", // "search.filters.filter.namedresourcetype.placeholder": "Status", - // TODO New key - Add a translation - "search.filters.filter.namedresourcetype.placeholder": "Status", - + "search.filters.filter.namedresourcetype.placeholder": "Statut", // "search.filters.filter.objectpeople.head": "People", - // TODO New key - Add a translation - "search.filters.filter.objectpeople.head": "People", - + "search.filters.filter.objectpeople.head": "Personne", // "search.filters.filter.objectpeople.placeholder": "People", - // TODO New key - Add a translation - "search.filters.filter.objectpeople.placeholder": "People", - + "search.filters.filter.objectpeople.placeholder": "Personne", // "search.filters.filter.organizationAddressCountry.head": "Country", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressCountry.head": "Country", - + "search.filters.filter.organizationAddressCountry.head": "Pays", // "search.filters.filter.organizationAddressCountry.placeholder": "Country", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressCountry.placeholder": "Country", - + "search.filters.filter.organizationAddressCountry.placeholder": "Pays", // "search.filters.filter.organizationAddressLocality.head": "City", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressLocality.head": "City", - + "search.filters.filter.organizationAddressLocality.head": "Ville", // "search.filters.filter.organizationAddressLocality.placeholder": "City", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressLocality.placeholder": "City", - + "search.filters.filter.organizationAddressLocality.placeholder": "Ville", // "search.filters.filter.organizationFoundingDate.head": "Date Founded", - // TODO New key - Add a translation - "search.filters.filter.organizationFoundingDate.head": "Date Founded", - + "search.filters.filter.organizationFoundingDate.head": "Date de fondation", // "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", - // TODO New key - Add a translation - "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", - + "search.filters.filter.organizationFoundingDate.placeholder": "Date de fondation", // "search.filters.filter.scope.head": "Scope", - // TODO New key - Add a translation "search.filters.filter.scope.head": "Scope", - // "search.filters.filter.scope.placeholder": "Scope filter", - // TODO New key - Add a translation - "search.filters.filter.scope.placeholder": "Scope filter", - + "search.filters.filter.scope.placeholder": "Scope", // "search.filters.filter.show-less": "Collapse", - // TODO New key - Add a translation - "search.filters.filter.show-less": "Collapse", - + "search.filters.filter.show-less": "Moins", // "search.filters.filter.show-more": "Show more", - // TODO New key - Add a translation - "search.filters.filter.show-more": "Show more", - + "search.filters.filter.show-more": "Plus", // "search.filters.filter.subject.head": "Subject", - // TODO New key - Add a translation - "search.filters.filter.subject.head": "Subject", - + "search.filters.filter.subject.head": "Sujet", // "search.filters.filter.subject.placeholder": "Subject", - // TODO New key - Add a translation - "search.filters.filter.subject.placeholder": "Subject", - + "search.filters.filter.subject.placeholder": "Sujet", // "search.filters.filter.submitter.head": "Submitter", - // TODO New key - Add a translation - "search.filters.filter.submitter.head": "Submitter", - + "search.filters.filter.submitter.head": "Déposant", // "search.filters.filter.submitter.placeholder": "Submitter", - // TODO New key - Add a translation - "search.filters.filter.submitter.placeholder": "Submitter", - - - + "search.filters.filter.submitter.placeholder": "Déposant", // "search.filters.head": "Filters", - // TODO New key - Add a translation - "search.filters.head": "Filters", - + "search.filters.head": "Filtres", // "search.filters.reset": "Reset filters", - // TODO New key - Add a translation - "search.filters.reset": "Reset filters", - - - + "search.filters.reset": "Réinitialiser filtres", // "search.form.search": "Search", - // TODO New key - Add a translation - "search.form.search": "Search", - + "search.form.search": "Recherche", // "search.form.search_dspace": "Search DSpace", - // TODO New key - Add a translation - "search.form.search_dspace": "Search DSpace", - + "search.form.search_dspace": "Recherche dans DSpace", // "search.form.search_mydspace": "Search MyDSpace", - // TODO New key - Add a translation - "search.form.search_mydspace": "Search MyDSpace", - - - + "search.form.search_mydspace": "Recherche dans MyDSpace", // "search.results.head": "Search Results", - // TODO New key - Add a translation - "search.results.head": "Search Results", - + "search.results.head": "Résultats de recherche", // "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", - // TODO New key - Add a translation - "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", - + "search.results.no-results": "Votre recherche n'a retourné aucun résultat. Vous n'arrivez pas à trouver ce que vous recherchez ? Essayez de mettre", // "search.results.no-results-link": "quotes around it", - // TODO New key - Add a translation - "search.results.no-results-link": "quotes around it", - - - + "search.results.no-results-link": "des guillemets avant et après les termes recherchés", // "search.sidebar.close": "Back to results", - // TODO New key - Add a translation - "search.sidebar.close": "Back to results", - + "search.sidebar.close": "Retour aux résultats", // "search.sidebar.filters.title": "Filters", - // TODO New key - Add a translation - "search.sidebar.filters.title": "Filters", - + "search.sidebar.filters.title": "Filtres", // "search.sidebar.open": "Search Tools", - // TODO New key - Add a translation - "search.sidebar.open": "Search Tools", - + "search.sidebar.open": "Outils de recherche", // "search.sidebar.results": "results", - // TODO New key - Add a translation - "search.sidebar.results": "results", - + "search.sidebar.results": "Résultats", // "search.sidebar.settings.rpp": "Results per page", - // TODO New key - Add a translation - "search.sidebar.settings.rpp": "Results per page", - + "search.sidebar.settings.rpp": "Résultats par page", // "search.sidebar.settings.sort-by": "Sort By", - // TODO New key - Add a translation - "search.sidebar.settings.sort-by": "Sort By", - + "search.sidebar.settings.sort-by": "Trier par", // "search.sidebar.settings.title": "Settings", - // TODO New key - Add a translation - "search.sidebar.settings.title": "Settings", - - - + "search.sidebar.settings.title": "Paramètres", // "search.view-switch.show-detail": "Show detail", - // TODO New key - Add a translation - "search.view-switch.show-detail": "Show detail", - + "search.view-switch.show-detail": "Afficher détail", // "search.view-switch.show-grid": "Show as grid", - // TODO New key - Add a translation - "search.view-switch.show-grid": "Show as grid", - + "search.view-switch.show-grid": "Affichage Grille", // "search.view-switch.show-list": "Show as list", - // TODO New key - Add a translation - "search.view-switch.show-list": "Show as list", - - - + "search.view-switch.show-list": "Affichage Liste", // "sorting.dc.title.ASC": "Title Ascending", - // TODO New key - Add a translation - "sorting.dc.title.ASC": "Title Ascending", - + "sorting.dc.title.ASC": "Titre croissant", // "sorting.dc.title.DESC": "Title Descending", - // TODO New key - Add a translation - "sorting.dc.title.DESC": "Title Descending", - + "sorting.dc.title.DESC": "Titre décroissant", // "sorting.score.DESC": "Relevance", - // TODO New key - Add a translation - "sorting.score.DESC": "Relevance", - - - + "sorting.score.DESC": "Pertinence", + // "submission.edit.title": "Edit Submission", - // TODO New key - Add a translation - "submission.edit.title": "Edit Submission", - + "submission.edit.title": "Éditer dépôt", // "submission.general.cannot_submit": "You have not the privilege to make a new submission.", - // TODO New key - Add a translation - "submission.general.cannot_submit": "You have not the privilege to make a new submission.", - + "submission.general.cannot_submit": "Vous n'avez pas les droits requis pour réaliser un nouveau dépôt.", // "submission.general.deposit": "Deposit", - // TODO New key - Add a translation - "submission.general.deposit": "Deposit", - + "submission.general.deposit": "Déposer", // "submission.general.discard.confirm.cancel": "Cancel", - // TODO New key - Add a translation - "submission.general.discard.confirm.cancel": "Cancel", - + "submission.general.discard.confirm.cancel": "Annuler", // "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", - // TODO New key - Add a translation - "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", - + "submission.general.discard.confirm.info": "L'annulation est irréversible, les données du dépôt en cours seront perdues. Êtes-vous sûr(e) ?", // "submission.general.discard.confirm.submit": "Yes, I'm sure", - // TODO New key - Add a translation - "submission.general.discard.confirm.submit": "Yes, I'm sure", - + "submission.general.discard.confirm.submit": "Oui, certain(e)", // "submission.general.discard.confirm.title": "Discard submission", - // TODO New key - Add a translation - "submission.general.discard.confirm.title": "Discard submission", - + "submission.general.discard.confirm.title": "Annuler dépôt", // "submission.general.discard.submit": "Discard", - // TODO New key - Add a translation - "submission.general.discard.submit": "Discard", - + "submission.general.discard.submit": "Annuler", // "submission.general.save": "Save", - // TODO New key - Add a translation - "submission.general.save": "Save", - + "submission.general.save": "Sauvegarder", // "submission.general.save-later": "Save for later", - // TODO New key - Add a translation - "submission.general.save-later": "Save for later", - - - + "submission.general.save-later": "Sauvegarder pour plus tard", // "submission.sections.general.add-more": "Add more", - // TODO New key - Add a translation - "submission.sections.general.add-more": "Add more", - + "submission.sections.general.add-more": "Ajouter section(s)", // "submission.sections.general.collection": "Collection", - // TODO New key - Add a translation "submission.sections.general.collection": "Collection", - // "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", - // TODO New key - Add a translation - "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", - + "submission.sections.general.deposit_error_notice": "Une erreur s'est produite lors du dépôt de l'Item, veuillez réessayer plus tard.", // "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", - // TODO New key - Add a translation - "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", - + "submission.sections.general.deposit_success_notice": "Dépôt réalisé avec succès.", // "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", - // TODO New key - Add a translation - "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", - + "submission.sections.general.discard_error_notice": "Une erreur s'est produite lors de la suppression du brouillon, veuillez réessayer plus tard.", // "submission.sections.general.discard_success_notice": "Submission discarded successfully.", - // TODO New key - Add a translation - "submission.sections.general.discard_success_notice": "Submission discarded successfully.", - + "submission.sections.general.discard_success_notice": "Brouillon supprimé avec succès.", // "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", - // TODO New key - Add a translation - "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", - + "submission.sections.general.metadata-extracted": "De nouvelles métadonnées ont été extraites et ajoutées à la section {{sectionId}}.", // "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", - // TODO New key - Add a translation - "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", - + "submission.sections.general.metadata-extracted-new-section": "Nouvelle section {{sectionId}} ajoutée au dépôt.", // "submission.sections.general.no-collection": "No collection found", - // TODO New key - Add a translation - "submission.sections.general.no-collection": "No collection found", - + "submission.sections.general.no-collection": "Aucune Collection disponible", // "submission.sections.general.no-sections": "No options available", - // TODO New key - Add a translation - "submission.sections.general.no-sections": "No options available", - + "submission.sections.general.no-sections": "Aucune option disponible", // "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", - // TODO New key - Add a translation - "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", - + "submission.sections.general.save_error_notice": "Une erreur s'est produite à la sauvegarde du dépôt, veuillez réessayer plus tard.", // "submission.sections.general.save_success_notice": "Submission saved successfully.", - // TODO New key - Add a translation - "submission.sections.general.save_success_notice": "Submission saved successfully.", - + "submission.sections.general.save_success_notice": "Dépôt sauvegardé avec succès.", // "submission.sections.general.search-collection": "Search for a collection", - // TODO New key - Add a translation - "submission.sections.general.search-collection": "Search for a collection", - + "submission.sections.general.search-collection": "Rechercher une Collection", // "submission.sections.general.sections_not_valid": "There are incomplete sections.", - // TODO New key - Add a translation - "submission.sections.general.sections_not_valid": "There are incomplete sections.", - - - + "submission.sections.general.sections_not_valid": "Section(s) incomplète(s).", // "submission.sections.submit.progressbar.cclicense": "Creative commons license", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.cclicense": "Creative commons license", - + "submission.sections.submit.progressbar.cclicense": "Licence Creative Commons", // "submission.sections.submit.progressbar.describe.recycle": "Recycle", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.recycle": "Recycle", - + "submission.sections.submit.progressbar.describe.recycle": "Recycler", // "submission.sections.submit.progressbar.describe.stepcustom": "Describe", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.stepcustom": "Describe", - + "submission.sections.submit.progressbar.describe.stepcustom": "Décrire", // "submission.sections.submit.progressbar.describe.stepone": "Describe", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.stepone": "Describe", - + "submission.sections.submit.progressbar.describe.stepone": "Décrire", // "submission.sections.submit.progressbar.describe.steptwo": "Describe", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.steptwo": "Describe", - + "submission.sections.submit.progressbar.describe.steptwo": "Décrire", // "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", - + "submission.sections.submit.progressbar.detect-duplicate": "Doublon(s) potentiel(s)", // "submission.sections.submit.progressbar.license": "Deposit license", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.license": "Deposit license", - + "submission.sections.submit.progressbar.license": "Licence de dépôt", // "submission.sections.submit.progressbar.upload": "Upload files", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.upload": "Upload files", - - - + "submission.sections.submit.progressbar.upload": "Uploader", // "submission.sections.upload.delete.confirm.cancel": "Cancel", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.cancel": "Cancel", - + "submission.sections.upload.delete.confirm.cancel": "Annuler", // "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", - + "submission.sections.upload.delete.confirm.info": "Cette opération est irréversible. Êtes-vous sûr(e) ?", // "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", - + "submission.sections.upload.delete.confirm.submit": "Oui, certain(e)", // "submission.sections.upload.delete.confirm.title": "Delete bitstream", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.title": "Delete bitstream", - + "submission.sections.upload.delete.confirm.title": "Supprimer Bitstream", // "submission.sections.upload.delete.submit": "Delete", - // TODO New key - Add a translation - "submission.sections.upload.delete.submit": "Delete", - + "submission.sections.upload.delete.submit": "Supprimer", // "submission.sections.upload.drop-message": "Drop files to attach them to the item", - // TODO New key - Add a translation - "submission.sections.upload.drop-message": "Drop files to attach them to the item", - + "submission.sections.upload.drop-message": "Déposer des fichiers pour les lier à l'Item", // "submission.sections.upload.form.access-condition-label": "Access condition type", - // TODO New key - Add a translation - "submission.sections.upload.form.access-condition-label": "Access condition type", - + "submission.sections.upload.form.access-condition-label": "Niveau d'accès", // "submission.sections.upload.form.date-required": "Date is required.", - // TODO New key - Add a translation - "submission.sections.upload.form.date-required": "Date is required.", - + "submission.sections.upload.form.date-required": "Date obligatoire.", // "submission.sections.upload.form.from-label": "Access grant from", - // TODO New key - Add a translation - "submission.sections.upload.form.from-label": "Access grant from", - + "submission.sections.upload.form.from-label": "Accès accordé depuis", // "submission.sections.upload.form.from-placeholder": "From", - // TODO New key - Add a translation - "submission.sections.upload.form.from-placeholder": "From", - + "submission.sections.upload.form.from-placeholder": "Depuis", // "submission.sections.upload.form.group-label": "Group", - // TODO New key - Add a translation - "submission.sections.upload.form.group-label": "Group", - + "submission.sections.upload.form.group-label": "Groupe", // "submission.sections.upload.form.group-required": "Group is required.", - // TODO New key - Add a translation - "submission.sections.upload.form.group-required": "Group is required.", - + "submission.sections.upload.form.group-required": "Groupe requis.", // "submission.sections.upload.form.until-label": "Access grant until", - // TODO New key - Add a translation - "submission.sections.upload.form.until-label": "Access grant until", - + "submission.sections.upload.form.until-label": "Accès accordé jusque", // "submission.sections.upload.form.until-placeholder": "Until", - // TODO New key - Add a translation - "submission.sections.upload.form.until-placeholder": "Until", - + "submission.sections.upload.form.until-placeholder": "Jusque", // "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", - // TODO New key - Add a translation - "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", - + "submission.sections.upload.header.policy.default.nolist": "Les fichiers uploadés dans la Collection {{collectionName}} seront accessibles au(x) groupe(s) suivant(s) :", // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", - // TODO New key - Add a translation - "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", - + "submission.sections.upload.header.policy.default.withlist": "Veuillez noter que, en complément des accès spécifiquement accordés pour le fichier, les fichiers uploadés dans la Collection {{collectionName}} seront accessibles par défaut au(x) groupe(s) suivant(s) :", // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", - // TODO New key - Add a translation - "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", - + "submission.sections.upload.info": "Vous trouverez ici tous les fichiers actuellement associés à l'Item. Vous pouvez éditer les métadonnés et les niveaux d'accès de ce(s) fichier(s) ou uploader des fichiers complémentaires simplement en les glissant n'importe où sur cette page", // "submission.sections.upload.no-entry": "No", - // TODO New key - Add a translation - "submission.sections.upload.no-entry": "No", - + "submission.sections.upload.no-entry": "Non", // "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", - // TODO New key - Add a translation - "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", - + "submission.sections.upload.no-file-uploaded": "Aucun fichier uploadé.", // "submission.sections.upload.save-metadata": "Save metadata", - // TODO New key - Add a translation - "submission.sections.upload.save-metadata": "Save metadata", - + "submission.sections.upload.save-metadata": "Sauvegarder métadonnées", // "submission.sections.upload.undo": "Cancel", - // TODO New key - Add a translation - "submission.sections.upload.undo": "Cancel", - + "submission.sections.upload.undo": "Annuler", // "submission.sections.upload.upload-failed": "Upload failed", - // TODO New key - Add a translation - "submission.sections.upload.upload-failed": "Upload failed", - + "submission.sections.upload.upload-failed": "Échec de l'upload", // "submission.sections.upload.upload-successful": "Upload successful", - // TODO New key - Add a translation - "submission.sections.upload.upload-successful": "Upload successful", - - - + "submission.sections.upload.upload-successful": "Upload réussi", // "submission.submit.title": "Submission", - // TODO New key - Add a translation - "submission.submit.title": "Submission", - - + "submission.submit.title": "Dépôt", // "submission.workflow.generic.delete": "Delete", - // TODO New key - Add a translation - "submission.workflow.generic.delete": "Delete", - + "submission.workflow.generic.delete": "Supprimer", // "submission.workflow.generic.delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.", - // TODO New key - Add a translation - "submission.workflow.generic.delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.", - + "submission.workflow.generic.delete-help": "Si vous souhaitez supprimer cet Item, cliquez sur « Supprimer ». Une confirmation vous sera alors demandée.", // "submission.workflow.generic.edit": "Edit", - // TODO New key - Add a translation - "submission.workflow.generic.edit": "Edit", - + "submission.workflow.generic.edit": "Éditer", // "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", - // TODO New key - Add a translation - "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", - + "submission.workflow.generic.edit-help": "Sélectionner cette option pour modifier les métadonnées de l'Item.", // "submission.workflow.generic.view": "View", - // TODO New key - Add a translation - "submission.workflow.generic.view": "View", - + "submission.workflow.generic.view": "Afficher", // "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", - // TODO New key - Add a translation - "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", - - + "submission.workflow.generic.view-help": "Sélectionner cette option pour afficher les métadonnées de l'Item.", // "submission.workflow.tasks.claimed.approve": "Approve", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.approve": "Approve", - + "submission.workflow.tasks.claimed.approve": "Approuver", // "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", - + "submission.workflow.tasks.claimed.approve_help": "Si vous avez contrôlé cet Item et qu'il peut être ajouté à la Collection, sélectionner l'option « Approuver ».", // "submission.workflow.tasks.claimed.edit": "Edit", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.edit": "Edit", - + "submission.workflow.tasks.claimed.edit": "Éditer", // "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", - + "submission.workflow.tasks.claimed.edit_help": "Sélectionner cette option pour modifier les métadonnées de l'Item.", // "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", - + "submission.workflow.tasks.claimed.reject.reason.info": "Veuillez spécifier la raison pour laquelle ce dépôt est rejeté dans le champ ci-dessous, en indicant si le déposant peut résoudre un problème et redéposer ensuite.", // "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", - + "submission.workflow.tasks.claimed.reject.reason.placeholder": "Décrire la raison du rejet", // "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", - + "submission.workflow.tasks.claimed.reject.reason.submit": "Rejeter Item", // "submission.workflow.tasks.claimed.reject.reason.title": "Reason", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.title": "Reason", - + "submission.workflow.tasks.claimed.reject.reason.title": "Raison", // "submission.workflow.tasks.claimed.reject.submit": "Reject", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.submit": "Reject", - + "submission.workflow.tasks.claimed.reject.submit": "Rejeter", // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", - + "submission.workflow.tasks.claimed.reject_help": "Si vous avez contrôle cet Item et qu'il ne peut PAS être ajouté à la Collection, sélectionner l'option « Rejeter ». Il vous sera alors demandé d'indiquer la raison de ce rejet et si le déposant peut apporter une correction avant de redéposer.", // "submission.workflow.tasks.claimed.return": "Return to pool", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.return": "Return to pool", - + "submission.workflow.tasks.claimed.return": "Se désassigner", // "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", - - - + "submission.workflow.tasks.claimed.return_help": "Renvoyer la tâche dans le pool de validation pour qu'un autre utilisateur puisse en prendre soin.", // "submission.workflow.tasks.generic.error": "Error occurred during operation...", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.error": "Error occurred during operation...", - + "submission.workflow.tasks.generic.error": "Une erreur s'est produite au cours de l'opération...", // "submission.workflow.tasks.generic.processing": "Processing...", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.processing": "Processing...", - + "submission.workflow.tasks.generic.processing": "En cours de traitement...", // "submission.workflow.tasks.generic.submitter": "Submitter", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.submitter": "Submitter", - + "submission.workflow.tasks.generic.submitter": "Déposant", // "submission.workflow.tasks.generic.success": "Operation successful", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.success": "Operation successful", - - - + "submission.workflow.tasks.generic.success": "Opération exécutée avec succès", // "submission.workflow.tasks.pool.claim": "Claim", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.claim": "Claim", - + "submission.workflow.tasks.pool.claim": "Prendre en charge", // "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", - + "submission.workflow.tasks.pool.claim_help": "Assigner cette tâche à votre propre utilisateur.", // "submission.workflow.tasks.pool.hide-detail": "Hide detail", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.hide-detail": "Hide detail", - + "submission.workflow.tasks.pool.hide-detail": "Masquer détails", // "submission.workflow.tasks.pool.show-detail": "Show detail", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.show-detail": "Show detail", - - + "submission.workflow.tasks.pool.show-detail": "Afficher détails", // "title": "DSpace", - // TODO New key - Add a translation "title": "DSpace", - - - - // "uploader.browse": "browse", - // TODO New key - Add a translation - "uploader.browse": "browse", - - // "uploader.drag-message": "Drag & Drop your files here", - // TODO New key - Add a translation - "uploader.drag-message": "Drag & Drop your files here", - - // "uploader.or": ", or", - // TODO New key - Add a translation - "uploader.or": ", or", - - // "uploader.processing": "Processing", - // TODO New key - Add a translation - "uploader.processing": "Processing", - - // "uploader.queue-length": "Queue length", - // TODO New key - Add a translation - "uploader.queue-length": "Queue length", - - - -} \ No newline at end of file + // "uploader.browse": "browse", + "uploader.browse": "parcourir", + // "uploader.drag-message": "Drag & Drop your files here", + "uploader.drag-message": "Glisser & Déposer vos fichiers ici", + // "uploader.or": ", or", + "uploader.or": ", ou", + // "uploader.processing": "Processing", + "uploader.processing": "En cours de traitement", + // "uploader.queue-length": "Queue length", + "uploader.queue-length": "Longueur de la file d'attente", + +} diff --git a/src/assets/i18n/lv.json5 b/src/assets/i18n/lv.json5 new file mode 100644 index 0000000000..6a564bf5bb --- /dev/null +++ b/src/assets/i18n/lv.json5 @@ -0,0 +1,3234 @@ +{ + + // "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": "Meklēto lapu nav iespējams atrast. Iespējams, lapa ir pārvietota vai izdzēsta. Lai atgrieztos sākumlapā, varat izmantot zemāk esošo pogu. ", + + // "404.link.home-page": "Take me to the home page", + "404.link.home-page": "Argriezties uz sākumu", + + // "404.page-not-found": "page not found", + "404.page-not-found": "lapa nav atrasta", + + + + // "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", + "admin.registries.bitstream-formats.create.failure.content": "Veidojot jauno bitu straumes formātu, radās kļūda.", + + // "admin.registries.bitstream-formats.create.failure.head": "Failure", + "admin.registries.bitstream-formats.create.failure.head": "Kļūda", + + // "admin.registries.bitstream-formats.create.head": "Create Bitstream format", + "admin.registries.bitstream-formats.create.head": "Izveidot Bitu straumes formātu", + + // "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", + "admin.registries.bitstream-formats.create.new": "Pievienot jaunu bitu straumes formātu", + + // "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", + "admin.registries.bitstream-formats.create.success.content": "Jauns bitu straumes formāts tika veiksmīgi izveidots.", + + // "admin.registries.bitstream-formats.create.success.head": "Success", + "admin.registries.bitstream-formats.create.success.head": "Veiksmīgi izpildīts", + + // "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", + "admin.registries.bitstream-formats.delete.failure.amount": "Neizdevās dzēst {{ amount }} formātu(s)", + + // "admin.registries.bitstream-formats.delete.failure.head": "Failure", + "admin.registries.bitstream-formats.delete.failure.head": "Kļūda", + + // "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", + "admin.registries.bitstream-formats.delete.success.amount": "Veiksmīgi dzēsti {{ amount }} formats(i)", + + // "admin.registries.bitstream-formats.delete.success.head": "Success", + "admin.registries.bitstream-formats.delete.success.head": "Veiksmīgi izpildīts", + + // "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", + "admin.registries.bitstream-formats.description": "Šis bitu straumes formātu saraksts sniedz informāciju par zināmajiem formātiem un to atbalsta līmeni.", + + // "admin.registries.bitstream-formats.edit.description.hint": "", + "admin.registries.bitstream-formats.edit.description.hint": "", + + // "admin.registries.bitstream-formats.edit.description.label": "Description", + "admin.registries.bitstream-formats.edit.description.label": "Apraksts", + + // "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": "Paplašinājumi ir failu paplašinājumi, kurus izmanto, lai automātiski identificētu augšupielādēto failu formātu. Katram formātam var ievadīt vairākus paplašinājumus.", + + // "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", + "admin.registries.bitstream-formats.edit.extensions.label": "Faila paplašinājums", + + // "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot", + "admin.registries.bitstream-formats.edit.extensions.placeholder": "Ievadiet faila paplašinājumu bez punkta", + + // "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", + "admin.registries.bitstream-formats.edit.failure.content": "Rediģējot bitu straumes formātu, radās kļūda.", + + // "admin.registries.bitstream-formats.edit.failure.head": "Failure", + "admin.registries.bitstream-formats.edit.failure.head": "Kļūda", + + // "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", + "admin.registries.bitstream-formats.edit.head": "Bitu straumes formāts: {{ 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": "Formāti, kas atzīmēti kā iekšēji, tiek paslēpti no lietotāja un tiek izmantoti administratīviem mērķiem.", + + // "admin.registries.bitstream-formats.edit.internal.label": "Internal", + "admin.registries.bitstream-formats.edit.internal.label": "Iekšējais", + + // "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", + "admin.registries.bitstream-formats.edit.mimetype.hint": "MIME tipam, kas saistīts ar šo formātu, nav jābūt unikālam.", + + // "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", + "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Tips", + + // "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", + "admin.registries.bitstream-formats.edit.shortDescription.hint": "Šim formātam ir unikāls nosaukums, (piem. Microsoft Word XP or Microsoft Word 2000)", + + // "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", + "admin.registries.bitstream-formats.edit.shortDescription.label": "Nosaukums", + + // "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", + "admin.registries.bitstream-formats.edit.success.content": "Bitu straumes formāts tika veiksmīgi rediģēts.", + + // "admin.registries.bitstream-formats.edit.success.head": "Success", + "admin.registries.bitstream-formats.edit.success.head": "Veiksmīgi izpildīts", + + // "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", + "admin.registries.bitstream-formats.edit.supportLevel.hint": "Atbalsta līmenis, ko jūsu iestāde apņemas nodrošināt šim formātam.", + + // "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", + "admin.registries.bitstream-formats.edit.supportLevel.label": "Atbalsta līmenis", + + // "admin.registries.bitstream-formats.head": "Bitstream Format Registry", + "admin.registries.bitstream-formats.head": "Bitu straumes Formatu Reģistrs", + + // "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", + "admin.registries.bitstream-formats.no-items": "Nav rādāmi bitu straumes formāti.", + + // "admin.registries.bitstream-formats.table.delete": "Delete selected", + "admin.registries.bitstream-formats.table.delete": "Dzēst atlasīto", + + // "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", + "admin.registries.bitstream-formats.table.deselect-all": "Noņemt atlasi", + + // "admin.registries.bitstream-formats.table.internal": "internal", + "admin.registries.bitstream-formats.table.internal": "Iekšējais", + + // "admin.registries.bitstream-formats.table.mimetype": "MIME Type", + "admin.registries.bitstream-formats.table.mimetype": "MIME Tips", + + // "admin.registries.bitstream-formats.table.name": "Name", + "admin.registries.bitstream-formats.table.name": "Nosaukums", + + // "admin.registries.bitstream-formats.table.return": "Return", + "admin.registries.bitstream-formats.table.return": "Atgriezties", + + // "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", + "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Zināms", + + // "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", + "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Atbalstīts", + + // "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", + "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Nezināms", + + // "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", + "admin.registries.bitstream-formats.table.supportLevel.head": "Atbalsta Līmenis", + + // "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", + "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitu straumes Formatu Reģistrs", + + + + // "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": "Metadatu reģistrā tiek uzturēts visu repozitorijā pieejamo metadatu lauku saraksts. Šos laukus var sadalīt starp vairākām shēmām. Tomēr DSpace ir nepieciešama kvalificēta Dublin Core shēma.", + + // "admin.registries.metadata.form.create": "Create metadata schema", + "admin.registries.metadata.form.create": "Izveidot metadatu shēmu", + + // "admin.registries.metadata.form.edit": "Edit metadata schema", + "admin.registries.metadata.form.edit": "Rediģēt metadatu shēmu", + + // "admin.registries.metadata.form.name": "Name", + "admin.registries.metadata.form.name": "Nosaukums", + + // "admin.registries.metadata.form.namespace": "Namespace", + "admin.registries.metadata.form.namespace": "Nosaukumvieta", + + // "admin.registries.metadata.head": "Metadata Registry", + "admin.registries.metadata.head": "Metadatu reģistrs", + + // "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", + "admin.registries.metadata.schemas.no-items": "Nav rādāmas metadatu shēmas.", + + // "admin.registries.metadata.schemas.table.delete": "Delete selected", + "admin.registries.metadata.schemas.table.delete": "Dzēst izvēlēto", + + // "admin.registries.metadata.schemas.table.id": "ID", + "admin.registries.metadata.schemas.table.id": "ID", + + // "admin.registries.metadata.schemas.table.name": "Name", + "admin.registries.metadata.schemas.table.name": "Nosaukums", + + // "admin.registries.metadata.schemas.table.namespace": "Namespace", + "admin.registries.metadata.schemas.table.namespace": "Nosaukumvieta", + + // "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", + "admin.registries.metadata.title": "DSpace Angular :: Metadatu Registry", + + + + // "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", + "admin.registries.schema.description": "Metadu shēma priekš \"{{namespace}}\".", + + // "admin.registries.schema.fields.head": "Schema metadata fields", + "admin.registries.schema.fields.head": "Shēmas metadatu lauki", + + // "admin.registries.schema.fields.no-items": "No metadata fields to show.", + "admin.registries.schema.fields.no-items": "Nav pieejami metadatu lauki.", + + // "admin.registries.schema.fields.table.delete": "Delete selected", + "admin.registries.schema.fields.table.delete": "Dzēst izvēlēto", + + // "admin.registries.schema.fields.table.field": "Field", + "admin.registries.schema.fields.table.field": "Lauks", + + // "admin.registries.schema.fields.table.scopenote": "Scope Note", + "admin.registries.schema.fields.table.scopenote": "Jomas Piezīme", + + // "admin.registries.schema.form.create": "Create metadata field", + "admin.registries.schema.form.create": "Izveidot matadatu lauku", + + // "admin.registries.schema.form.edit": "Edit metadata field", + "admin.registries.schema.form.edit": "Rediģēt metadatu lauku", + + // "admin.registries.schema.form.element": "Element", + "admin.registries.schema.form.element": "Elements", + + // "admin.registries.schema.form.qualifier": "Qualifier", + "admin.registries.schema.form.qualifier": "Kvalifikācija", + + // "admin.registries.schema.form.scopenote": "Scope Note", + "admin.registries.schema.form.scopenote": "Jomas Piezīme", + + // "admin.registries.schema.head": "Metadata Schema", + "admin.registries.schema.head": "Metadatu Shēma", + + // "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", + "admin.registries.schema.notification.created": "Veiksmīgi izveidota metadatu shēma \"{{prefix}}\"", + + // "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", + "admin.registries.schema.notification.deleted.failure": "Neizdevās izdzēst {{amount}} metadatu shēmas", + + // "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", + "admin.registries.schema.notification.deleted.success": "{{amount}} metadatu shēmas ir veiksmīgi izdzēstas", + + // "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", + "admin.registries.schema.notification.edited": "Veiksmīgi rediģēta metadatu shēma \"{{prefix}}\"", + + // "admin.registries.schema.notification.failure": "Error", + "admin.registries.schema.notification.failure": "Kļūda", + + // "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", + "admin.registries.schema.notification.field.created": "Metadatu lauks ir veiksmīgi izveidots \"{{field}}\"", + + // "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", + "admin.registries.schema.notification.field.deleted.failure": "Neizdevās izdzēst {{amount}} metadatu laukus", + + // "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", + "admin.registries.schema.notification.field.deleted.success": "{{amount}} metadatu lauki ir veiksmīgi izdzēsti", + + // "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", + "admin.registries.schema.notification.field.edited": "Metadatu lauks ir veiksmīgi rediģēts \"{{field}}\"", + + // "admin.registries.schema.notification.success": "Success", + "admin.registries.schema.notification.success": "Veiksmīgi izpildīts", + + // "admin.registries.schema.return": "Return", + "admin.registries.schema.return": "Atgriezties", + + // "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", + "admin.registries.schema.title": "DSpace Angular :: Metadatu shēmas reģistrs", + + + + // "admin.access-control.epeople.title": "DSpace Angular :: EPeople", + "admin.access-control.epeople.title": "DSpace Angular :: EPersonas", + + // "admin.access-control.epeople.head": "EPeople", + "admin.access-control.epeople.head": "EPersonas", + + // "admin.access-control.epeople.search.head": "Search", + "admin.access-control.epeople.search.head": "Meklēt", + + // "admin.access-control.epeople.search.scope.name": "Name", + "admin.access-control.epeople.search.scope.name": "Vārds", + + // "admin.access-control.epeople.search.scope.email": "E-mail (exact)", + "admin.access-control.epeople.search.scope.email": "E-pasts (pilns)", + + // "admin.access-control.epeople.search.scope.metadata": "Metadata", + "admin.access-control.epeople.search.scope.metadata": "Metadati", + + // "admin.access-control.epeople.search.button": "Search", + "admin.access-control.epeople.search.button": "Meklēt", + + // "admin.access-control.epeople.button.add": "Add EPerson", + "admin.access-control.epeople.button.add": "Pievienot EPersonu", + + // "admin.access-control.epeople.table.id": "ID", + "admin.access-control.epeople.table.id": "ID", + + // "admin.access-control.epeople.table.name": "Name", + "admin.access-control.epeople.table.name": "Lietotājs", + + // "admin.access-control.epeople.table.email": "E-mail", + "admin.access-control.epeople.table.email": "E-pasts", + + // "admin.access-control.epeople.table.edit": "Edit", + "admin.access-control.epeople.table.edit": "Rediģēt", + + // "item.access-control.epeople.table.edit.buttons.edit": "Edit", + "item.access-control.epeople.table.edit.buttons.edit": "Rediģēt", + + // "item.access-control.epeople.table.edit.buttons.remove": "Remove", + "item.access-control.epeople.table.edit.buttons.remove": "Dzēst", + + // "admin.access-control.epeople.no-items": "No EPeople to show.", + "admin.access-control.epeople.no-items": "Nav pieejamas EPersonas.", + + // "admin.access-control.epeople.form.create": "Create EPerson", + "admin.access-control.epeople.form.create": "Izveidot EPersonu", + + // "admin.access-control.epeople.form.edit": "Edit EPerson", + "admin.access-control.epeople.form.edit": "Rediģēt EPersonu", + + // "admin.access-control.epeople.form.firstName": "First name", + "admin.access-control.epeople.form.firstName": "Vārds", + + // "admin.access-control.epeople.form.lastName": "Last name", + "admin.access-control.epeople.form.lastName": "Uzvārds", + + // "admin.access-control.epeople.form.email": "E-mail", + "admin.access-control.epeople.form.email": "E-pasts", + + // "admin.access-control.epeople.form.emailHint": "Must be valid e-mail address", + "admin.access-control.epeople.form.emailHint": "Jābūt derīgai e-pasta adresei", + + // "admin.access-control.epeople.form.canLogIn": "Can log in", + "admin.access-control.epeople.form.canLogIn": "Var pierakstīties", + + // "admin.access-control.epeople.form.requireCertificate": "Requires certificate", + "admin.access-control.epeople.form.requireCertificate": "Nepieciešams sertifikāts", + + // "admin.access-control.epeople.form.notification.created.success": "Successfully created EPerson \"{{name}}\"", + "admin.access-control.epeople.form.notification.created.success": "Veiksmīgi izveidota EPersona \"{{name}}\"", + + // "admin.access-control.epeople.form.notification.created.failure": "Failed to create EPerson \"{{name}}\"", + "admin.access-control.epeople.form.notification.created.failure": "Neizdevās izveidot EPersonu \"{{name}}\"", + + // "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": "Neizdevās izveidot EPersonu \"{{name}}\", e-pasts \"{{email}}\" jau tiek lietots.", + + // "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": "Neizdevās rediģēt EPersonu \"{{name}}\", e-pasts \"{{email}}\" jau tiek lietots.", + + // "admin.access-control.epeople.form.notification.edited.success": "Successfully edited EPerson \"{{name}}\"", + "admin.access-control.epeople.form.notification.edited.success": "Veiksmīgi rediģēta EPersona \"{{name}}\"", + + // "admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"", + "admin.access-control.epeople.form.notification.edited.failure": "Neizdevās rediģēt EPerosnu \"{{name}}\"", + + // "admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"", + "admin.access-control.epeople.notification.deleted.failure": "Neizdevās dzēst EPersonu: \"{{name}}\"", + + // "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"", + "admin.access-control.epeople.notification.deleted.success": "Veiksmīgi dzēsta EPersona: \"{{name}}\"", + + + + // "admin.search.breadcrumbs": "Administrative Search", + "admin.search.breadcrumbs": "Administratīvā Meklēšana", + + // "admin.search.collection.edit": "Edit", + "admin.search.collection.edit": "Rediģēt", + + // "admin.search.community.edit": "Edit", + "admin.search.community.edit": "Rediģēt", + + // "admin.search.item.delete": "Delete", + "admin.search.item.delete": "Dzēst", + + // "admin.search.item.edit": "Edit", + "admin.search.item.edit": "Rediģēt", + + // "admin.search.item.make-private": "Make Private", + "admin.search.item.make-private": "Padarīt Privātu", + + // "admin.search.item.make-public": "Make Public", + "admin.search.item.make-public": "Padarīt Publisku", + + // "admin.search.item.move": "Move", + "admin.search.item.move": "Pārvietot", + + // "admin.search.item.private": "Private", + "admin.search.item.private": "Privāts", + + // "admin.search.item.reinstate": "Reinstate", + "admin.search.item.reinstate": "Atjaunot", + + // "admin.search.item.withdraw": "Withdraw", + "admin.search.item.withdraw": "Atsaukt", + + // "admin.search.item.withdrawn": "Withdrawn", + "admin.search.item.withdrawn": "Atsaukts", + + // "admin.search.title": "Administrative Search", + "admin.search.title": "Administratīvā Meklēšana", + + + + // "auth.errors.invalid-user": "Invalid email address or password.", + "auth.errors.invalid-user": "Nepareizs e-pasta adrese vai parole.", + + // "auth.messages.expired": "Your session has expired. Please log in again.", + "auth.messages.expired": "Jūsu sesija ir beigusies. Lūdzu pieslēdzieties vēlreiz", + + + + // "browse.comcol.by.author": "By Author", + "browse.comcol.by.author": "Pēc Autora", + + // "browse.comcol.by.dateissued": "By Issue Date", + "browse.comcol.by.dateissued": "Pēc Izdošanas Datuma", + + // "browse.comcol.by.subject": "By Subject", + "browse.comcol.by.subject": "Pēc Priekšmeta", + + // "browse.comcol.by.title": "By Title", + "browse.comcol.by.title": "Pēc Nosaukuma", + + // "browse.comcol.head": "Browse", + "browse.comcol.head": "Pārlūkot", + + // "browse.empty": "No items to show.", + "browse.empty": "Ieraksti nav atrasti.", + + // "browse.metadata.author": "Author", + "browse.metadata.author": "Autors", + + // "browse.metadata.dateissued": "Issue Date", + "browse.metadata.dateissued": "Izdošanas datums", + + // "browse.metadata.subject": "Subject", + "browse.metadata.subject": "Priekšmets", + + // "browse.metadata.title": "Title", + "browse.metadata.title": "Nosaukums", + + // "browse.metadata.author.breadcrumbs": "Browse by Author", + "browse.metadata.author.breadcrumbs": "Meklēt pēc Autora", + + // "browse.metadata.dateissued.breadcrumbs": "Browse by Date", + "browse.metadata.dateissued.breadcrumbs": "Meklēt pēc Datuma", + + // "browse.metadata.subject.breadcrumbs": "Browse by Subject", + "browse.metadata.subject.breadcrumbs": "Meklēt pēc Priekšmeta", + + // "browse.metadata.title.breadcrumbs": "Browse by Title", + "browse.metadata.title.breadcrumbs": "Meklēt pēc Nosaukuma", + + // "browse.startsWith.choose_start": "(Choose start)", + "browse.startsWith.choose_start": "(Izvēlieties sākumu)", + + // "browse.startsWith.choose_year": "(Choose year)", + "browse.startsWith.choose_year": "(Izvēlieties gadu)", + + // "browse.startsWith.jump": "Jump to a point in the index:", + "browse.startsWith.jump": "Pāriet uz punktu indeksā:", + + // "browse.startsWith.months.april": "April", + "browse.startsWith.months.april": "Aprīlis", + + // "browse.startsWith.months.august": "August", + "browse.startsWith.months.august": "Augusts", + + // "browse.startsWith.months.december": "December", + "browse.startsWith.months.december": "Decembris", + + // "browse.startsWith.months.february": "February", + "browse.startsWith.months.february": "Februāris", + + // "browse.startsWith.months.january": "January", + "browse.startsWith.months.january": "Janvāris", + + // "browse.startsWith.months.july": "July", + "browse.startsWith.months.july": "Jūlijs", + + // "browse.startsWith.months.june": "June", + "browse.startsWith.months.june": "Jūnijs", + + // "browse.startsWith.months.march": "March", + "browse.startsWith.months.march": "Marts", + + // "browse.startsWith.months.may": "May", + "browse.startsWith.months.may": "Maijs", + + // "browse.startsWith.months.none": "(Choose month)", + "browse.startsWith.months.none": "(Izvēlieties mēnesi)", + + // "browse.startsWith.months.november": "November", + "browse.startsWith.months.november": "Novembris", + + // "browse.startsWith.months.october": "October", + "browse.startsWith.months.october": "Oktobris", + + // "browse.startsWith.months.september": "September", + "browse.startsWith.months.september": "Septembris", + + // "browse.startsWith.submit": "Go", + "browse.startsWith.submit": "Izpildīt", + + // "browse.startsWith.type_date": "Or type in a date (year-month):", + "browse.startsWith.type_date": "Vai ievadiet datumu (gads-mēnesis):", + + // "browse.startsWith.type_text": "Or enter first few letters:", + "browse.startsWith.type_text": "Vai arī ievadiet pirmos burtus:", + + // "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", + "browse.title": "Meklē {{ collection }} pēc {{ field }} {{ value }}", + + + // "chips.remove": "Remove chip", + "chips.remove": "Dzēst daļu", + + + + // "collection.create.head": "Create a Collection", + "collection.create.head": "Izveidot Kolekciju", + + // "collection.create.notifications.success": "Successfully created the Collection", + "collection.create.notifications.success": "Kolekcija tika veiksmīgi izveidota", + + // "collection.create.sub-head": "Create a Collection for Community {{ parent }}", + "collection.create.sub-head": "Izveidot Kolekciju priekš Kategorijas {{ parent }}", + + // "collection.delete.cancel": "Cancel", + "collection.delete.cancel": "Atcelt", + + // "collection.delete.confirm": "Confirm", + "collection.delete.confirm": "Apstiprināt", + + // "collection.delete.head": "Delete Collection", + "collection.delete.head": "Dzēst Kolekciju", + + // "collection.delete.notification.fail": "Collection could not be deleted", + "collection.delete.notification.fail": "Kolekciju nevar izdzēst", + + // "collection.delete.notification.success": "Successfully deleted collection", + "collection.delete.notification.success": "Kolekcija ir veiksmīgi izdzēsta", + + // "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", + "collection.delete.text": "Vai esat pārliecināts, ka vēlaties izdzēst kolekciju \"{{ dso }}\"", + + + + // "collection.edit.delete": "Delete this collection", + "collection.edit.delete": "Dzēst šo kolekciju", + + // "collection.edit.head": "Edit Collection", + "collection.edit.head": "Rediģēt Kolekciju", + + // "collection.edit.breadcrumbs": "Edit Collection", + "collection.edit.breadcrumbs": "Rediģēt Kolekciju", + + + + // "collection.edit.item-mapper.cancel": "Cancel", + "collection.edit.item-mapper.cancel": "Atcelt", + + // "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", + "collection.edit.item-mapper.collection": "Kolekcija: \"{{name}}\"", + + // "collection.edit.item-mapper.confirm": "Map selected items", + "collection.edit.item-mapper.confirm": "Piesaistīt izvēlētos materiālus", + + // "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", + "collection.edit.item-mapper.description": "Šis ir materiālu piesaistīšanas rīks, kas kolekciju administratoriem ļauj šajā kolekcijā piesaistīt citu kolekciju materiālus. Jūs varat meklēt materiālus no citām kolekcijām un piesaistīt tos vai pārlūkot pašlaik piesaistīto materiālu sarakstu.", + + // "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", + "collection.edit.item-mapper.head": "Materiālu piesaistīšana - Materiālu piesaistīšana no citas kolekcijas", + + // "collection.edit.item-mapper.no-search": "Please enter a query to search", + "collection.edit.item-mapper.no-search": "Lūdzu ievadiet vaicājumu,lai meklētu", + + // "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", + "collection.edit.item-mapper.notifications.map.error.content": "{{amount}} materiālu piesaistīšanā radās kļūdas.", + + // "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", + "collection.edit.item-mapper.notifications.map.error.head": "Piesaistīšanas kļūdas", + + // "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", + "collection.edit.item-mapper.notifications.map.success.content": "Veiksmīgi piesaistīti {{amount}} materiāli.", + + // "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", + "collection.edit.item-mapper.notifications.map.success.head": "Piesaistīšana pabeigta", + + // "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", + "collection.edit.item-mapper.notifications.unmap.error.content": "Noņemot piesaistītos materiālus {{amount}}, radās kļūda.", + + // "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", + "collection.edit.item-mapper.notifications.unmap.error.head": "Dzēst piesaistes kļūdas", + + // "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", + "collection.edit.item-mapper.notifications.unmap.success.content": "Veiksmīgi dzēsta piesaiste {{amount}} materiāliem.", + + // "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", + "collection.edit.item-mapper.notifications.unmap.success.head": "Piesaistes dzēšana izpildīta", + + // "collection.edit.item-mapper.remove": "Remove selected item mappings", + "collection.edit.item-mapper.remove": "Dzēst izvēlēto materiālu piesaistes", + + // "collection.edit.item-mapper.tabs.browse": "Browse mapped items", + "collection.edit.item-mapper.tabs.browse": "Pārlūkot piesaistītos materiālus", + + // "collection.edit.item-mapper.tabs.map": "Map new items", + "collection.edit.item-mapper.tabs.map": "Piesaistīt jaunus materiālus", + + + + // "collection.edit.logo.label": "Collection logo", + "collection.edit.logo.label": "Kolekcijas logotips", + + // "collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.", + "collection.edit.logo.notifications.add.error": "Kolekcijas logotipa augšupielāde neizdevās. Pirms mēģināt vēlreiz, lūdzu, pārbaudiet saturu.", + + // "collection.edit.logo.notifications.add.success": "Upload Collection logo successful.", + "collection.edit.logo.notifications.add.success": "Kolekcijas logotipa augšupielāde ir veiksmīga.", + + // "collection.edit.logo.notifications.delete.success.title": "Logo deleted", + "collection.edit.logo.notifications.delete.success.title": "Logotips dzēsts", + + // "collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo", + "collection.edit.logo.notifications.delete.success.content": "Kolekcijas logotips ir veiksmīgi izdzēsts", + + // "collection.edit.logo.notifications.delete.error.title": "Error deleting logo", + "collection.edit.logo.notifications.delete.error.title": "Kļūda dzēšot logotopu", + + // "collection.edit.logo.upload": "Drop a Collection Logo to upload", + "collection.edit.logo.upload": "Ievietot Kolekcijas logotipu, lai augšupielādētu", + + + + // "collection.edit.notifications.success": "Successfully edited the Collection", + "collection.edit.notifications.success": "Kolekcija ir veiksmīgi rediģēta", + + // "collection.edit.return": "Return", + "collection.edit.return": "Atgriezties", + + + + // "collection.edit.tabs.curate.head": "Curate", + "collection.edit.tabs.curate.head": "Pārvaldīt", + + // "collection.edit.tabs.curate.title": "Collection Edit - Curate", + "collection.edit.tabs.curate.title": "Rediģēt kolekciju - Pārvaldība", + + // "collection.edit.tabs.metadata.head": "Edit Metadata", + "collection.edit.tabs.metadata.head": "Rediģēt Metadatus", + + // "collection.edit.tabs.metadata.title": "Collection Edit - Metadata", + "collection.edit.tabs.metadata.title": "Rediģēt Kolekciju - Metadati", + + // "collection.edit.tabs.roles.head": "Assign Roles", + "collection.edit.tabs.roles.head": "Piešķirt Lomu", + + // "collection.edit.tabs.roles.title": "Collection Edit - Roles", + "collection.edit.tabs.roles.title": "Rediģēt Kolekciju - Lomas", + + // "collection.edit.tabs.source.external": "This collection harvests its content from an external source", + "collection.edit.tabs.source.external": "Šīs kolekcijas saturs tiek iegūts no ārēja avota", + + // "collection.edit.tabs.source.form.errors.oaiSource.required": "You must provide a set id of the target collection.", + "collection.edit.tabs.source.form.errors.oaiSource.required": "Jums ir jānorāda noteikts id no mērķa kolekcijas", + + // "collection.edit.tabs.source.form.harvestType": "Content being harvested", + "collection.edit.tabs.source.form.harvestType": "Saturs tiek iegūts", + + // "collection.edit.tabs.source.form.head": "Configure an external source", + "collection.edit.tabs.source.form.head": "Konfigurēt ārējo avotu", + + // "collection.edit.tabs.source.form.metadataConfigId": "Metadata Format", + "collection.edit.tabs.source.form.metadataConfigId": "Matadatu formāts", + + // "collection.edit.tabs.source.form.oaiSetId": "OAI specific set id", + "collection.edit.tabs.source.form.oaiSetId": "OAI konkrētas kopas id", + + // "collection.edit.tabs.source.form.oaiSource": "OAI Provider", + "collection.edit.tabs.source.form.oaiSource": "OAI Sniedzējs", + + // "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_BITSTREAMS": "Harvest metadata and bitstreams (requires ORE support)", + "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_BITSTREAMS": "Ievākt metadatus un bitu straumes (nepieciešams ORE atbalsts)", + + // "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_REF": "Harvest metadata and references to bitstreams (requires ORE support)", + "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_REF": "Ievākt metadatus un atsauces uz bitu straumēm (requires ORE support)", + + // "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Harvest metadata only", + "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Iegūt tikai metadatus", + + // "collection.edit.tabs.source.head": "Content Source", + "collection.edit.tabs.source.head": "Satura Avots", + + // "collection.edit.tabs.source.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + "collection.edit.tabs.source.notifications.discarded.content": "Jūsu veiktās izmaiņas tika atmestas. Lai atjaunotu izmaiņas, noklikšķiniet uz pogas 'Atsaukt'", + + // "collection.edit.tabs.source.notifications.discarded.title": "Changed discarded", + "collection.edit.tabs.source.notifications.discarded.title": "Mainīts atmests", + + // "collection.edit.tabs.source.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", + "collection.edit.tabs.source.notifications.invalid.content": "Jūsu veiktās izmaiņas netika saglabātas. Pirms saglabāšanas, lūdzu, pārliecinieties, vai visi lauki ir derīgi.", + + // "collection.edit.tabs.source.notifications.invalid.title": "Metadata invalid", + "collection.edit.tabs.source.notifications.invalid.title": "Metadati nav derīgi", + + // "collection.edit.tabs.source.notifications.saved.content": "Your changes to this collection's content source were saved.", + "collection.edit.tabs.source.notifications.saved.content": "Jūsu veiktās izmaiņas šīs kolekcijas satura avotā tika saglabātas.", + + // "collection.edit.tabs.source.notifications.saved.title": "Content Source saved", + "collection.edit.tabs.source.notifications.saved.title": "Satura avots ir saglabāts", + + // "collection.edit.tabs.source.title": "Collection Edit - Content Source", + "collection.edit.tabs.source.title": "Rediģēt Kolekciju - Satura Avots", + + + + // "collection.form.abstract": "Short Description", + "collection.form.abstract": "Īss apraksts", + + // "collection.form.description": "Introductory text (HTML)", + "collection.form.description": "Ievadteksts (HTML)", + + // "collection.form.errors.title.required": "Please enter a collection name", + "collection.form.errors.title.required": "Lūdzu ievadiet kolekcijas nosaukumu", + + // "collection.form.license": "License", + "collection.form.license": "Licence", + + // "collection.form.provenance": "Provenance", + "collection.form.provenance": "Izcelsmes avots", + + // "collection.form.rights": "Copyright text (HTML)", + "collection.form.rights": "Autortiesību teksts (HTML)", + + // "collection.form.tableofcontents": "News (HTML)", + "collection.form.tableofcontents": "Jaunumi (HTML)", + + // "collection.form.title": "Name", + "collection.form.title": "Nosaukums", + + + + // "collection.page.browse.recent.head": "Recent Submissions", + "collection.page.browse.recent.head": "Nesenie iesniegumi", + + // "collection.page.browse.recent.empty": "No items to show", + "collection.page.browse.recent.empty": "Ieraksti nav atrasti", + + // "collection.page.handle": "Permanent URI for this collection", + "collection.page.handle": "Kolekcijas nemainīgs URI", + + // "collection.page.license": "License", + "collection.page.license": "Licence", + + // "collection.page.news": "News", + "collection.page.news": "Jaunumi", + + + + // "collection.select.confirm": "Confirm selected", + "collection.select.confirm": "Apsitprināt izvēlēto", + + // "collection.select.empty": "No collections to show", + "collection.select.empty": "Kolekcijas nav pieejamas", + + // "collection.select.table.title": "Title", + "collection.select.table.title": "Nosaukums", + + + + // "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.", + "collection.source.update.notifications.error.content": "Norādītie iestatījumi ir pārbaudīti un nedarbojas.", + + // "collection.source.update.notifications.error.title": "Server Error", + "collection.source.update.notifications.error.title": "Servera Kļūda", + + + + // "communityList.tabTitle": "DSpace - Community List", + "communityList.tabTitle": "DSpace - Kategoriju Saraksts", + + // "communityList.title": "List of Communities", + "communityList.title": "Kategoriju saraksts", + + // "communityList.showMore": "Show More", + "communityList.showMore": "Rādīt Vairāk", + + + + // "community.create.head": "Create a Community", + "community.create.head": "Izveidot Kategoriju", + + // "community.create.notifications.success": "Successfully created the Community", + "community.create.notifications.success": "Kategorija tika veiksmīgi izveidota", + + // "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", + "community.create.sub-head": "Izveidot Apakškategorija priekš Kategorijas {{ parent }}", + + // "community.delete.cancel": "Cancel", + "community.delete.cancel": "Atcelt", + + // "community.delete.confirm": "Confirm", + "community.delete.confirm": "Apstiprināt", + + // "community.delete.head": "Delete Community", + "community.delete.head": "Dzēst Kategoriju", + + // "community.delete.notification.fail": "Community could not be deleted", + "community.delete.notification.fail": "Kategriju nav iespējams izdzēst", + + // "community.delete.notification.success": "Successfully deleted community", + "community.delete.notification.success": "Veiksmīgi izdzēsta kategorija", + + // "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", + "community.delete.text": "Vai tiešām vēlaties dzēst kategoriju \"{{ dso }}\"", + + // "community.edit.delete": "Delete this community", + "community.edit.delete": "Dzēst šo kategoriju", + + // "community.edit.head": "Edit Community", + "community.edit.head": "Rediģēt Kategoriju", + + // "community.edit.breadcrumbs": "Edit Community", + "community.edit.breadcrumbs": "Rediģēt Kategoriju", + + + // "community.edit.logo.label": "Community logo", + "community.edit.logo.label": "Kategorijas logotips", + + // "community.edit.logo.notifications.add.error": "Uploading Community logo failed. Please verify the content before retrying.", + "community.edit.logo.notifications.add.error": "Kategorijas logotipa augšupielāde neizdevās. Pirms mēģināt vēlreiz, lūdzu, pārbaudiet saturu.", + + // "community.edit.logo.notifications.add.success": "Upload Community logo successful.", + "community.edit.logo.notifications.add.success": "Kategorijas logotipa augšupielāde ir veiksmīga.", + + // "community.edit.logo.notifications.delete.success.title": "Logo deleted", + "community.edit.logo.notifications.delete.success.title": "logotips ir izdzēsts", + + // "community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo", + "community.edit.logo.notifications.delete.success.content": "Kategorijas logotips ir veiksmīgi izdzēsts", + + // "community.edit.logo.notifications.delete.error.title": "Error deleting logo", + "community.edit.logo.notifications.delete.error.title": "Izdzēšot logotipu, radās kļūda", + + // "community.edit.logo.upload": "Drop a Community Logo to upload", + "community.edit.logo.upload": "Ievietot Kategorijas logotipu, lai augšupielādētu", + + + + // "community.edit.notifications.success": "Successfully edited the Community", + "community.edit.notifications.success": "Kategorija tika veiksmīgi rediģēta", + + // "community.edit.return": "Return", + "community.edit.return": "Atgriezties", + + + + // "community.edit.tabs.curate.head": "Curate", + "community.edit.tabs.curate.head": "Pārvaldīt", + + // "community.edit.tabs.curate.title": "Community Edit - Curate", + "community.edit.tabs.curate.title": "Rdiģēt Kategoriju - Pārvaldība", + + // "community.edit.tabs.metadata.head": "Edit Metadata", + "community.edit.tabs.metadata.head": "Rediģēt Metadatus", + + // "community.edit.tabs.metadata.title": "Community Edit - Metadata", + "community.edit.tabs.metadata.title": "Rediģēt Kategoriju - Metadati", + + // "community.edit.tabs.roles.head": "Assign Roles", + "community.edit.tabs.roles.head": "Piešķirt Lomas", + + // "community.edit.tabs.roles.title": "Community Edit - Roles", + "community.edit.tabs.roles.title": "Rediģēt Kategorju - Lomas", + + + + // "community.form.abstract": "Short Description", + "community.form.abstract": "Īss apraksts", + + // "community.form.description": "Introductory text (HTML)", + "community.form.description": "Ievadteksts (HTML)", + + // "community.form.errors.title.required": "Please enter a community name", + "community.form.errors.title.required": "Lūdzu ievadiet kategorijas nosaukumu", + + // "community.form.rights": "Copyright text (HTML)", + "community.form.rights": "Autortiesību teksts (HTML)", + + // "community.form.tableofcontents": "News (HTML)", + "community.form.tableofcontents": "Jaunumi (HTML)", + + // "community.form.title": "Name", + "community.form.title": "Nosaukums", + + // "community.page.handle": "Permanent URI for this community", + "community.page.handle": "Kategorijas nemainīgs URI", + + // "community.page.license": "License", + "community.page.license": "Licence", + + // "community.page.news": "News", + "community.page.news": "Jaunumi", + + // "community.all-lists.head": "Subcommunities and Collections", + "community.all-lists.head": "Apakškategorijas un Kolekcijas", + + // "community.sub-collection-list.head": "Collections of this Community", + "community.sub-collection-list.head": "Kolekcijas no šīs kategorijas", + + // "community.sub-community-list.head": "Communities of this Community", + "community.sub-community-list.head": "Šīs kategorijas apakš-kategorijas", + + + + // "dso-selector.create.collection.head": "New collection", + "dso-selector.create.collection.head": "Jauna kolekcija", + + // "dso-selector.create.community.head": "New community", + "dso-selector.create.community.head": "Jauna kategorija", + + // "dso-selector.create.community.sub-level": "Create a new community in", + "dso-selector.create.community.sub-level": "Izveidot jaunu kategroiju iekš", + + // "dso-selector.create.community.top-level": "Create a new top-level community", + "dso-selector.create.community.top-level": "Izveidot jaunu augstākā līmeņa kategoriju", + + // "dso-selector.create.item.head": "New item", + "dso-selector.create.item.head": "Jauns materiāls", + + // "dso-selector.edit.collection.head": "Edit collection", + "dso-selector.edit.collection.head": "Rediģēt kolekciju", + + // "dso-selector.edit.community.head": "Edit community", + "dso-selector.edit.community.head": "Rediģēt kategoriju", + + // "dso-selector.edit.item.head": "Edit item", + "dso-selector.edit.item.head": "Rediģēt matriālu", + + // "dso-selector.no-results": "No {{ type }} found", + "dso-selector.no-results": "Nav atrasts {{ type }}", + + // "dso-selector.placeholder": "Search for a {{ type }}", + "dso-selector.placeholder": "Meklēt {{ type }}", + + + + // "error.browse-by": "Error fetching items", + "error.browse-by": "Materiālu ielasīšanas kļūda", + + // "error.collection": "Error fetching collection", + "error.collection": "Kolekciju ielasīšanas kļūda", + + // "error.collections": "Error fetching collections", + "error.collections": "Kolekciju ielasīšanas kļūda", + + // "error.community": "Error fetching community", + "error.community": "Kategorijas ielasīšanas kļūda", + + // "error.identifier": "No item found for the identifier", + "error.identifier": "Netika atrasti materiāli dotajam identifikatoram", + + // "error.default": "Error", + "error.default": "Kļūda", + + // "error.item": "Error fetching item", + "error.item": "Materiāla ielasīšanas kļūda", + + // "error.items": "Error fetching items", + "error.items": "Materiālu ielasīšanas kļūda", + + // "error.objects": "Error fetching objects", + "error.objects": "Objektu ielasīšanas kļūda", + + // "error.recent-submissions": "Error fetching recent submissions", + "error.recent-submissions": "Neseno iesniegumu ielasīšanas kļūda", + + // "error.search-results": "Error fetching search results", + "error.search-results": "Meklēšanas rezultātu ielasīšanas kļūda", + + // "error.sub-collections": "Error fetching sub-collections", + "error.sub-collections": "Kļūda ielasot apakškolekcijas", + + // "error.sub-communities": "Error fetching sub-communities", + "error.sub-communities": "Apakškategorijas ielasīšanas kļūda", + + // "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

", + "error.submission.sections.init-form-error": "Sadaļas inicializācijas laikā radās kļūda. Lūdzu, pārbaudiet ievades formas konfigurāciju. Sīkāka informācija ir sniegta zemāk :

", + + // "error.top-level-communities": "Error fetching top-level communities", + "error.top-level-communities": "Kļūda ielasot augstākā līmeņa kategorijas", + + // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", + "error.validation.license.notgranted": "Jums ir jānodrošina šī licence, lai pabeigtu iesniegšanu. Ja šobrīd nevarat piešķirt šo licenci, varat saglabāt savu darbu un atgriezties vēlāk vai noņemt iesniegšanu.", + + // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", + "error.validation.pattern": "Šo ievadi ierobežo pašreizējais modelis: {{ pattern }}.", + + // "error.validation.filerequired": "The file upload is mandatory", + "error.validation.filerequired": "Faila augšupielāde ir obligāta", + + + + // "footer.copyright": "copyright © 2002-{{ year }}", + "footer.copyright": "copyright © 2002-{{ year }}", + + // "footer.link.dspace": "DSpace software", + "footer.link.dspace": "DSpace software", + + // "footer.link.duraspace": "DuraSpace", + "footer.link.duraspace": "DuraSpace", + + + // "form.add": "Add", + "form.add": "Pievienot", + + // "form.add-help": "Click here to add the current entry and to add another one", + "form.add-help": "Noklikšķiniet šeit, lai pievienotu tekošo elementu un lai pievienotu vēl vienu", + + // "form.cancel": "Cancel", + "form.cancel": "Atcelt", + + // "form.clear": "Clear", + "form.clear": "Notīrīt", + + // "form.clear-help": "Click here to remove the selected value", + "form.clear-help": "Noklikšķiniet šeit, lai dzēstu atlasīto vērtību", + + // "form.edit": "Edit", + "form.edit": "Rediģēt", + + // "form.edit-help": "Click here to edit the selected value", + "form.edit-help": "Noklikšķiniet šeit, lai rediģēt atlasīto vērtību", + + // "form.first-name": "First name", + "form.first-name": "Vārds", + + // "form.group-collapse": "Collapse", + "form.group-collapse": "Sakļaut", + + // "form.group-collapse-help": "Click here to collapse", + "form.group-collapse-help": "Noklikšķiniet šeit, lai sakļautu", + + // "form.group-expand": "Expand", + "form.group-expand": "Izvērst", + + // "form.group-expand-help": "Click here to expand and add more elements", + "form.group-expand-help": "Noklikšķiniet šeit, lai izvērstu un pievienotu citus elementus", + + // "form.last-name": "Last name", + "form.last-name": "Uzvārds", + + // "form.loading": "Loading...", + "form.loading": "Notiek ielāde...", + + // "form.lookup": "Lookup", + "form.lookup": "Atrast", + + // "form.lookup-help": "Click here to look up an existing relation", + "form.lookup-help": "Noklikšķiniet šeit, lai meklētu esošu saistību", + + // "form.no-results": "No results found", + "form.no-results": "Rezultāti netika atrasti", + + // "form.no-value": "No value entered", + "form.no-value": "Vērtība netika ievadīta", + + // "form.other-information": {}, + "form.other-information": {}, + + // "form.remove": "Remove", + "form.remove": "Dzēst", + + // "form.save": "Save", + "form.save": "Saglabāt", + + // "form.save-help": "Save changes", + "form.save-help": "Saglabāt izmaiņas", + + // "form.search": "Search", + "form.search": "Meklēt", + + // "form.search-help": "Click here to looking for an existing correspondence", + "form.search-help": "Noklikšķiniet šeit, lai meklētu esošu korespondenci", + + // "form.submit": "Submit", + "form.submit": "Iesniegt", + + + + // "home.description": "", + "home.description": "", + + // "home.title": "DSpace Angular :: Home", + "home.title": "DSpace Angular :: Sākums", + + // "home.top-level-communities.head": "Communities in DSpace", + "home.top-level-communities.head": "DSpace Kategorijas", + + // "home.top-level-communities.help": "Select a community to browse its collections.", + "home.top-level-communities.help": "Izvēlieties kategoriju, lai pārlūkotu tās kolekcijas.", + + + + // "item.edit.delete.cancel": "Cancel", + "item.edit.delete.cancel": "Atcelt", + + // "item.edit.delete.confirm": "Delete", + "item.edit.delete.confirm": "Dzēst", + + // "item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.", + "item.edit.delete.description": "Vai esat drošs, ka pilnībā vēlaties izdzēst šo ierakstu? Uzmanību: Ieraksta kopija netiks saglabāta.", + + // "item.edit.delete.error": "An error occurred while deleting the item", + "item.edit.delete.error": "Radās kļūda dzēšot materiālus", + + // "item.edit.delete.header": "Delete item: {{ id }}", + "item.edit.delete.header": "Dzēst materiālu: {{ id }}", + + // "item.edit.delete.success": "The item has been deleted", + "item.edit.delete.success": "Materiāls ir izdzēsts", + + // "item.edit.head": "Edit Item", + "item.edit.head": "Rediģēt Materiālu", + + // "item.edit.breadcrumbs": "Edit Item", + "item.edit.breadcrumbs": "Rediģēt Materiālu", + + + + // "item.edit.item-mapper.buttons.add": "Map item to selected collections", + "item.edit.item-mapper.buttons.add": "Piesaistīt materiālu piesaistītajai kolekcijai", + + // "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", + "item.edit.item-mapper.buttons.remove": "Dzēst materiālus izvēlētajai kolekcijai", + + // "item.edit.item-mapper.cancel": "Cancel", + "item.edit.item-mapper.cancel": "Atcelt", + + // "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", + "item.edit.item-mapper.description": "Šis ir materiālu piesaistīšanas rīks, kas ļauj administratoriem piesaistīt šo materiālu citās kolekcijās. Jūs varat meklēt kolekcijas un piesaistīt tās vai pārlūkot kolekciju sarakstu, uz kuru materiāls pašlaik ir piesaistīts.", + + // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", + "item.edit.item-mapper.head": "Materiāla piesaistīšana - Piesaistīt Materiālu Kolekcijai", + + // "item.edit.item-mapper.item": "Item: \"{{name}}\"", + "item.edit.item-mapper.item": "Materiāls: \"{{name}}\"", + + // "item.edit.item-mapper.no-search": "Please enter a query to search", + "item.edit.item-mapper.no-search": "Lūdzu ievadiet vaicājumu, lai meklētu", + + // "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", + "item.edit.item-mapper.notifications.add.error.content": "Radās kļūda piesaistot materiālus {{amount}} kolekcijai.", + + // "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", + "item.edit.item-mapper.notifications.add.error.head": "Piesaistīšanas kļūdas", + + // "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", + "item.edit.item-mapper.notifications.add.success.content": "Veiksmīgi piesaistīto materiāli {{amount}} kolekcijai.", + + // "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", + "item.edit.item-mapper.notifications.add.success.head": "Piesaistīšana pabeigta", + + // "item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.", + "item.edit.item-mapper.notifications.remove.error.content": "Rādās kļūdas noņemot piesaistes {{amount}} kolekcijām.", + + // "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", + "item.edit.item-mapper.notifications.remove.error.head": "Piesaistīšanas kļūdu noņemšana", + + // "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", + "item.edit.item-mapper.notifications.remove.success.content": "Veiksmīgi noņemta materiālu piesaiste {{amount}} kolekcijās.", + + // "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", + "item.edit.item-mapper.notifications.remove.success.head": "Piesaistes noņemšana ir pabeigta", + + // "item.edit.item-mapper.tabs.browse": "Browse mapped collections", + "item.edit.item-mapper.tabs.browse": "Pārlūkot piesaistīās kolekcijas", + + // "item.edit.item-mapper.tabs.map": "Map new collections", + "item.edit.item-mapper.tabs.map": "Piesaistīt jaunu kolekciju", + + + + // "item.edit.metadata.add-button": "Add", + "item.edit.metadata.add-button": "Pievienot", + + // "item.edit.metadata.discard-button": "Discard", + "item.edit.metadata.discard-button": "Atmest", + + // "item.edit.metadata.edit.buttons.edit": "Edit", + "item.edit.metadata.edit.buttons.edit": "Rediģēt", + + // "item.edit.metadata.edit.buttons.remove": "Remove", + "item.edit.metadata.edit.buttons.remove": "Dzēst", + + // "item.edit.metadata.edit.buttons.undo": "Undo changes", + "item.edit.metadata.edit.buttons.undo": "Atsaukt izmaiņas", + + // "item.edit.metadata.edit.buttons.unedit": "Stop editing", + "item.edit.metadata.edit.buttons.unedit": "Pārtraukt rediģēšanu", + + // "item.edit.metadata.headers.edit": "Edit", + "item.edit.metadata.headers.edit": "Rediģēt", + + // "item.edit.metadata.headers.field": "Field", + "item.edit.metadata.headers.field": "Lauks", + + // "item.edit.metadata.headers.language": "Lang", + "item.edit.metadata.headers.language": "Valoda", + + // "item.edit.metadata.headers.value": "Value", + "item.edit.metadata.headers.value": "Vērtība", + + // "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", + "item.edit.metadata.metadatafield.invalid": "Lūdzu, izvēlieties derīgu metadatu lauku", + + // "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + "item.edit.metadata.notifications.discarded.content": "Jūsu veiktās izmaiņas tika atmestas. Lai atjaunotu izmaiņas, noklikšķiniet uz pogas 'Atsaukt'", + + // "item.edit.metadata.notifications.discarded.title": "Changed discarded", + "item.edit.metadata.notifications.discarded.title": "Mainīts atmests", + + // "item.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", + "item.edit.metadata.notifications.invalid.content": "Jūsu veiktās izmaiņas netika saglabātas. Pirms saglabāšanas, lūdzu, pārliecinieties, vai visi lauki ir derīgi.", + + // "item.edit.metadata.notifications.invalid.title": "Metadata invalid", + "item.edit.metadata.notifications.invalid.title": "Metadati nav derīgi", + + // "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", + "item.edit.metadata.notifications.outdated.content": "Materiāls, kurā pašlaik strādājat, ir mainījis cits lietotājs. Jūsu pašreizējās izmaiņas tiek atmestas, lai novērstu konfliktus", + + // "item.edit.metadata.notifications.outdated.title": "Changed outdated", + "item.edit.metadata.notifications.outdated.title": "Mainīts novecojis", + + // "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", + "item.edit.metadata.notifications.saved.content": "Jūsu veiktās izmaiņas šī materiāla metadatos tika saglabātas.", + + // "item.edit.metadata.notifications.saved.title": "Metadata saved", + "item.edit.metadata.notifications.saved.title": "Metadati saglabāti", + + // "item.edit.metadata.reinstate-button": "Undo", + "item.edit.metadata.reinstate-button": "Atsaukt", + + // "item.edit.metadata.save-button": "Save", + "item.edit.metadata.save-button": "Saglabāt", + + + + // "item.edit.modify.overview.field": "Field", + "item.edit.modify.overview.field": "Lauks", + + // "item.edit.modify.overview.language": "Language", + "item.edit.modify.overview.language": "Valoda", + + // "item.edit.modify.overview.value": "Value", + "item.edit.modify.overview.value": "Vērtība", + + + + // "item.edit.move.cancel": "Cancel", + "item.edit.move.cancel": "Atcelt", + + // "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", + "item.edit.move.description": "Atlasiet kolekciju, uz kuru vēlaties pārvietot šo materiālu. Lai sašaurinātu parādīto kolekciju sarakstu, lodziņā varat ievadīt meklēšanas vaicājumu.", + + // "item.edit.move.error": "An error occurred when attempting to move the item", + "item.edit.move.error": "Mēģinot pārvietot materiālu, radās kļūda", + + // "item.edit.move.head": "Move item: {{id}}", + "item.edit.move.head": "Pārvietot materiālu: {{id}}", + + // "item.edit.move.inheritpolicies.checkbox": "Inherit policies", + "item.edit.move.inheritpolicies.checkbox": "Mantot nosacījumus", + + // "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", + "item.edit.move.inheritpolicies.description": "Mantot mērķa kolekcijas noklusējuma politikas", + + // "item.edit.move.move": "Move", + "item.edit.move.move": "Pārvietot", + + // "item.edit.move.processing": "Moving...", + "item.edit.move.processing": "Pārvieto...", + + // "item.edit.move.search.placeholder": "Enter a search query to look for collections", + "item.edit.move.search.placeholder": "Ievadiet meklēšanas vaicājumu, lai meklētu kolekcijas", + + // "item.edit.move.success": "The item has been moved successfully", + "item.edit.move.success": "Materiāls ir veiksmīgi pārvietots", + + // "item.edit.move.title": "Move item", + "item.edit.move.title": "Pārvietot materiālu", + + + + // "item.edit.private.cancel": "Cancel", + "item.edit.private.cancel": "Atcelt", + + // "item.edit.private.confirm": "Make it Private", + "item.edit.private.confirm": "Padarīt privātu", + + // "item.edit.private.description": "Are you sure this item should be made private in the archive?", + "item.edit.private.description": "Vai esat pārliecināts, ka šim materiālam arhīvā jābūt privātam?", + + // "item.edit.private.error": "An error occurred while making the item private", + "item.edit.private.error": "Padarot elementu privātu radās kļūda", + + // "item.edit.private.header": "Make item private: {{ id }}", + "item.edit.private.header": "Padarīt materiālu privātu: {{ id }}", + + // "item.edit.private.success": "The item is now private", + "item.edit.private.success": "Materiāls tagad ir privāts", + + + + // "item.edit.public.cancel": "Cancel", + "item.edit.public.cancel": "Atcelt", + + // "item.edit.public.confirm": "Make it Public", + "item.edit.public.confirm": "Padarīt Publisku", + + // "item.edit.public.description": "Are you sure this item should be made public in the archive?", + "item.edit.public.description": "Vai tiešām vēlaties šo vienumu publiskot arhīvā?", + + // "item.edit.public.error": "An error occurred while making the item public", + "item.edit.public.error": "Padarot elementu publisku radās kļūda", + + // "item.edit.public.header": "Make item public: {{ id }}", + "item.edit.public.header": "Padarīt materiālu publisku: {{ id }}", + + // "item.edit.public.success": "The item is now public", + "item.edit.public.success": "Materiāls tagad ir publisks", + + + + // "item.edit.reinstate.cancel": "Cancel", + "item.edit.reinstate.cancel": "Atcelt", + + // "item.edit.reinstate.confirm": "Reinstate", + "item.edit.reinstate.confirm": "Atjaunot", + + // "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", + "item.edit.reinstate.description": "Vai esat pārliecināts, ka šo materiālu nepieciešams atjaunot arhīvā?", + + // "item.edit.reinstate.error": "An error occurred while reinstating the item", + "item.edit.reinstate.error": "Atjaunot materiālu, radās kļūda", + + // "item.edit.reinstate.header": "Reinstate item: {{ id }}", + "item.edit.reinstate.header": "Atjaunot materiālu: {{ id }}", + + // "item.edit.reinstate.success": "The item was reinstated successfully", + "item.edit.reinstate.success": "Materiāls tika veiksmīgi atjaunots", + + + + // "item.edit.relationships.discard-button": "Discard", + "item.edit.relationships.discard-button": "Atcelt", + + // "item.edit.relationships.edit.buttons.remove": "Remove", + "item.edit.relationships.edit.buttons.remove": "Dzēst", + + // "item.edit.relationships.edit.buttons.undo": "Undo changes", + "item.edit.relationships.edit.buttons.undo": "Atsaukt izmaiņas", + + // "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + "item.edit.relationships.notifications.discarded.content": "Jūsu veiktās izmaiņas tika atmestas. Lai atjaunotu izmaiņas, noklikšķiniet uz pogas 'Atsaukt'", + + // "item.edit.relationships.notifications.discarded.title": "Changes discarded", + "item.edit.relationships.notifications.discarded.title": "Mainīts atmests", + + // "item.edit.relationships.notifications.failed.title": "Error deleting relationship", + "item.edit.relationships.notifications.failed.title": "Kļūda dzēšot saikni", + + // "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", + "item.edit.relationships.notifications.outdated.content": "Materiāls, kurā pašlaik strādājat, ir mainījis cits lietotājs. Jūsu pašreizējās izmaiņas tiek atmestas, lai novērstu konfliktus", + + // "item.edit.relationships.notifications.outdated.title": "Changes outdated", + "item.edit.relationships.notifications.outdated.title": "Novecojušas izmaiņas", + + // "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", + "item.edit.relationships.notifications.saved.content": "Jūsu veiktās izmaiņas šī materiāla saiknēs tika saglabātas.", + + // "item.edit.relationships.notifications.saved.title": "Relationships saved", + "item.edit.relationships.notifications.saved.title": "Saiknes saglabātas", + + // "item.edit.relationships.reinstate-button": "Undo", + "item.edit.relationships.reinstate-button": "Atsaukt", + + // "item.edit.relationships.save-button": "Save", + "item.edit.relationships.save-button": "Saglabāt", + + + + // "item.edit.tabs.bitstreams.head": "Item Bitstreams", + "item.edit.tabs.bitstreams.head": "Materiālu Bitstreams", + + // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", + "item.edit.tabs.bitstreams.title": "Rediģēt Materiālu - Bitu straumes", + + // "item.edit.tabs.curate.head": "Curate", + "item.edit.tabs.curate.head": "Pārvaldīt", + + // "item.edit.tabs.curate.title": "Item Edit - Curate", + "item.edit.tabs.curate.title": "Rediģēt Materiālu - Pārvaldība", + + // "item.edit.tabs.metadata.head": "Item Metadata", + "item.edit.tabs.metadata.head": "Materiāla Metadati", + + // "item.edit.tabs.metadata.title": "Item Edit - Metadata", + "item.edit.tabs.metadata.title": "Rediģēt Materiālu - Metadati", + + // "item.edit.tabs.relationships.head": "Item Relationships", + "item.edit.tabs.relationships.head": "Materiāla Attiecības", + + // "item.edit.tabs.relationships.title": "Item Edit - Relationships", + "item.edit.tabs.relationships.title": "Rediģēt Materiālu - Attiecības", + + // "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", + "item.edit.tabs.status.buttons.authorizations.button": "Tiesības...", + + // "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", + "item.edit.tabs.status.buttons.authorizations.label": "Rediģēt materiāla tiesības", + + // "item.edit.tabs.status.buttons.delete.button": "Permanently delete", + "item.edit.tabs.status.buttons.delete.button": "Neatgriezeniski izdzēst", + + // "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", + "item.edit.tabs.status.buttons.delete.label": "Pilnīgi izņemt materiālu", + + // "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", + "item.edit.tabs.status.buttons.mappedCollections.button": "Piesaistītās kolekcijas", + + // "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", + "item.edit.tabs.status.buttons.mappedCollections.label": "Pārvaldīt piesaistītās kolekcijas", + + // "item.edit.tabs.status.buttons.move.button": "Move...", + "item.edit.tabs.status.buttons.move.button": "Pārvieto...", + + // "item.edit.tabs.status.buttons.move.label": "Move item to another collection", + "item.edit.tabs.status.buttons.move.label": "Pārvietot materiālu uz citu kolekciju", + + // "item.edit.tabs.status.buttons.private.button": "Make it private...", + "item.edit.tabs.status.buttons.private.button": "Padarīt to privātu...", + + // "item.edit.tabs.status.buttons.private.label": "Make item private", + "item.edit.tabs.status.buttons.private.label": "Padarīt materiālu privātu", + + // "item.edit.tabs.status.buttons.public.button": "Make it public...", + "item.edit.tabs.status.buttons.public.button": "Padarīt to publisku...", + + // "item.edit.tabs.status.buttons.public.label": "Make item public", + "item.edit.tabs.status.buttons.public.label": "Padarīt materiālu publisku", + + // "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", + "item.edit.tabs.status.buttons.reinstate.button": "Atjaunot...", + + // "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", + "item.edit.tabs.status.buttons.reinstate.label": "Atjaunot materiālu repozitorijā", + + // "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", + "item.edit.tabs.status.buttons.withdraw.button": "Atsaukt...", + + // "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", + "item.edit.tabs.status.buttons.withdraw.label": "Izņemiet materiālu no repozitorijas", + + // "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", + "item.edit.tabs.status.description": "Laipni lūdzam vienumu pārvaldības lapā. Šeit jūs varat materiālu izņemt, atjaunot, pārvietot vai izdzēst. Citās cilnēs varat arī atjaunināt vai pievienot jaunus metadatus / bitu straumes.", + + // "item.edit.tabs.status.head": "Item Status", + "item.edit.tabs.status.head": "Materiāla Status", + + // "item.edit.tabs.status.labels.handle": "Handle", + "item.edit.tabs.status.labels.handle": "Apstrāde", + + // "item.edit.tabs.status.labels.id": "Item Internal ID", + "item.edit.tabs.status.labels.id": "Materiāla Iekšējais ID", + + // "item.edit.tabs.status.labels.itemPage": "Item Page", + "item.edit.tabs.status.labels.itemPage": "Materiāla Lapa", + + // "item.edit.tabs.status.labels.lastModified": "Last Modified", + "item.edit.tabs.status.labels.lastModified": "Pēdējo reizi modificēts", + + // "item.edit.tabs.status.title": "Item Edit - Status", + "item.edit.tabs.status.title": "Rediģēt Materiālu - Statuss", + + // "item.edit.tabs.versionhistory.head": "Version History", + "item.edit.tabs.versionhistory.head": "Versiju Vēsture", + + // "item.edit.tabs.versionhistory.title": "Item Edit - Version History", + "item.edit.tabs.versionhistory.title": "Rediģēt Materiālu - Versiju Vēsture", + + // "item.edit.tabs.versionhistory.under-construction": "Editing or adding new versions is not yet possible in this user interface.", + "item.edit.tabs.versionhistory.under-construction": "Šajā lietotāja saskarnē vēl nav iespējams ienākt vai pievienot jaunas versijas.", + + // "item.edit.tabs.view.head": "View Item", + "item.edit.tabs.view.head": "Skatīt Materiālu", + + // "item.edit.tabs.view.title": "Item Edit - View", + "item.edit.tabs.view.title": "Materiāla Rediģēšana - Skats", + + + + // "item.edit.withdraw.cancel": "Cancel", + "item.edit.withdraw.cancel": "Atcelt", + + // "item.edit.withdraw.confirm": "Withdraw", + "item.edit.withdraw.confirm": "Atsaukt", + + // "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", + "item.edit.withdraw.description": "Vai esat pārliecināts, ka šo materiālu vajadzētu izņemt no arhīva?", + + // "item.edit.withdraw.error": "An error occurred while withdrawing the item", + "item.edit.withdraw.error": "Izņemot materiālu, radās kļūda", + + // "item.edit.withdraw.header": "Withdraw item: {{ id }}", + "item.edit.withdraw.header": "Izņemt materiālu: {{ id }}", + + // "item.edit.withdraw.success": "The item was withdrawn successfully", + "item.edit.withdraw.success": "Materiāls tika veiksmīgi izņemts", + + + + // "item.page.abstract": "Abstract", + "item.page.abstract": "Kopsavilkums", + + // "item.page.author": "Authors", + "item.page.author": "Autori", + + // "item.page.citation": "Citation", + "item.page.citation": "Citēšana", + + // "item.page.collections": "Collections", + "item.page.collections": "Kolekcijas", + + // "item.page.date": "Date", + "item.page.date": "Datums", + + // "item.page.files": "Files", + "item.page.files": "Faili", + + // "item.page.filesection.description": "Description:", + "item.page.filesection.description": "Apraksts:", + + // "item.page.filesection.download": "Download", + "item.page.filesection.download": "Lejupielādēt", + + // "item.page.filesection.format": "Format:", + "item.page.filesection.format": "Formāts:", + + // "item.page.filesection.name": "Name:", + "item.page.filesection.name": "Nosaukums:", + + // "item.page.filesection.size": "Size:", + "item.page.filesection.size": "Izmērs:", + + // "item.page.journal.search.title": "Articles in this journal", + "item.page.journal.search.title": "Raksti šajā žurnālā", + + // "item.page.link.full": "Full item page", + "item.page.link.full": "Pilna materiālu lapa", + + // "item.page.link.simple": "Simple item page", + "item.page.link.simple": "Vienkārša materiālu lapa", + + // "item.page.person.search.title": "Articles by this author", + "item.page.person.search.title": "Šī autora raksti", + + // "item.page.related-items.view-more": "Show {{ amount }} more", + "item.page.related-items.view-more": "Parādīt {{ amount }} vairāk", + + // "item.page.related-items.view-less": "Hide last {{ amount }}", + "item.page.related-items.view-less": "Paslēpt pēdējo {{ amount }}", + + // "item.page.relationships.isAuthorOfPublication": "Publications", + "item.page.relationships.isAuthorOfPublication": "Publikācijas", + + // "item.page.relationships.isJournalOfPublication": "Publications", + "item.page.relationships.isJournalOfPublication": "Publikācijas", + + // "item.page.relationships.isOrgUnitOfPerson": "Authors", + "item.page.relationships.isOrgUnitOfPerson": "Autori", + + // "item.page.relationships.isOrgUnitOfProject": "Research Projects", + "item.page.relationships.isOrgUnitOfProject": "Pētniecības projekti", + + // "item.page.subject": "Keywords", + "item.page.subject": "Atslēgas vārdi", + + // "item.page.uri": "URI", + "item.page.uri": "URI", + + + + // "item.select.confirm": "Confirm selected", + "item.select.confirm": "Apstiprināt izvēlētos", + + // "item.select.empty": "No items to show", + "item.select.empty": "Ieraksti nav atrasti", + + // "item.select.table.author": "Author", + "item.select.table.author": "Autors", + + // "item.select.table.collection": "Collection",Research Projects + "item.select.table.collection": "Kolekcija", + + // "item.select.table.title": "Title", + "item.select.table.title": "Nosaukums", + + + // "item.version.history.empty": "There are no other versions for this item yet.", + "item.version.history.empty": "Šim materiālam vēl nav citu versiju.", + + // "item.version.history.head": "Version History", + "item.version.history.head": "Versiju Vēsture", + + // "item.version.history.return": "Return", + "item.version.history.return": "Atgriezties", + + // "item.version.history.selected": "Selected version", + "item.version.history.selected": "Izvēlētā versija", + + // "item.version.history.table.version": "Version", + "item.version.history.table.version": "Versija", + + // "item.version.history.table.item": "Item", + "item.version.history.table.item": "Materiāls", + + // "item.version.history.table.editor": "Editor", + "item.version.history.table.editor": "Redaktors", + + // "item.version.history.table.date": "Date", + "item.version.history.table.date": "Datums", + + // "item.version.history.table.summary": "Summary", + "item.version.history.table.summary": "Kopsavilkums", + + + + // "item.version.notice": "This is not the latest version of this item. The latest version can be found here.", + "item.version.notice": "Šī nav jaunākā šī materiāla versija. Jaunāko versiju var atrast here.", + + + + // "journal.listelement.badge": "Journal", + "journal.listelement.badge": "Žurnāls", + + // "journal.page.description": "Description", + "journal.page.description": "Apraksts", + + // "journal.page.editor": "Editor-in-Chief", + "journal.page.editor": "Galvenais Radaktors", + + // "journal.page.issn": "ISSN", + "journal.page.issn": "ISSN", + + // "journal.page.publisher": "Publisher", + "journal.page.publisher": "Izdevējs", + + // "journal.page.titleprefix": "Journal: ", + "journal.page.titleprefix": "Žurnāls: ", + + // "journal.search.results.head": "Journal Search Results", + "journal.search.results.head": "Žurnālu meklēšanas rezultāti", + + // "journal.search.title": "DSpace Angular :: Journal Search", + "journal.search.title": "DSpace Angular :: Žurnālu Meklēšana", + + + + // "journalissue.listelement.badge": "Journal Issue", + "journalissue.listelement.badge": "Žurnāla izdevums", + + // "journalissue.page.description": "Description", + "journalissue.page.description": "Apraksts", + + // "journalissue.page.issuedate": "Issue Date", + "journalissue.page.issuedate": "Izdošanas Datums", + + // "journalissue.page.journal-issn": "Journal ISSN", + "journalissue.page.journal-issn": "Žurnāla ISSN", + + // "journalissue.page.journal-title": "Journal Title", + "journalissue.page.journal-title": "Žurnāla nosaukums", + + // "journalissue.page.keyword": "Keywords", + "journalissue.page.keyword": "Atslēgas vārdi", + + // "journalissue.page.number": "Number", + "journalissue.page.number": "Numurs", + + // "journalissue.page.titleprefix": "Journal Issue: ", + "journalissue.page.titleprefix": "Žurnāla izdevums: ", + + + + // "journalvolume.listelement.badge": "Journal Volume", + "journalvolume.listelement.badge": "Žurnāla sējums", + + // "journalvolume.page.description": "Description", + "journalvolume.page.description": "Apraksts", + + // "journalvolume.page.issuedate": "Issue Date", + "journalvolume.page.issuedate": "Izdošanas Datums", + + // "journalvolume.page.titleprefix": "Journal Volume: ", + "journalvolume.page.titleprefix": "Žurnāla sējums: ", + + // "journalvolume.page.volume": "Volume", + "journalvolume.page.volume": "Sējums", + + + + // "loading.browse-by": "Loading items...", + "loading.browse-by": "Notiek materiālu ielāde...", + + // "loading.browse-by-page": "Loading page...", + "loading.browse-by-page": "Notiek lapas ielāde...", + + // "loading.collection": "Loading collection...", + "loading.collection": "Notiek kolekcijas ielāde...", + + // "loading.collections": "Loading collections...", + "loading.collections": "Notiek kolekciju ielāde...", + + // "loading.content-source": "Loading content source...", + "loading.content-source": "Notiek satura avota ielāde...", + + // "loading.community": "Loading community...", + "loading.community": "Notiek kategoriju ielāde...", + + // "loading.default": "Loading...", + "loading.default": "Notiek ielāde...", + + // "loading.item": "Loading item...", + "loading.item": "Notiek materiāla ielāde...", + + // "loading.items": "Loading items...", + "loading.items": "Notiek materiālu ielāde...", + + // "loading.mydspace-results": "Loading items...", + "loading.mydspace-results": "Notiek materiālu ielāde..", + + // "loading.objects": "Loading...", + "loading.objects": "Notiek ielāde...", + + // "loading.recent-submissions": "Loading recent submissions...", + "loading.recent-submissions": "Notiek neseno iesniegumu ielāde...", + + // "loading.search-results": "Loading search results...", + "loading.search-results": "Notiek maklēšanas rezulātu ielāde...", + + // "loading.sub-collections": "Loading sub-collections...", + "loading.sub-collections": "Notiek apakškolekciju ielāde...", + + // "loading.sub-communities": "Loading sub-communities...", + "loading.sub-communities": "Notiek apakškategoriju ielāde...", + + // "loading.top-level-communities": "Loading top-level communities...", + "loading.top-level-communities": "Notiek augstākā līmeņa kategorju ielāde...", + + + + // "login.form.email": "Email address", + "login.form.email": "E-pasta adrese", + + // "login.form.forgot-password": "Have you forgotten your password?", + "login.form.forgot-password": "Vai esat aizmirsis paroli?", + + // "login.form.header": "Please log in to DSpace", + "login.form.header": "Lūdzu pieslēdzieties DSpace", + + // "login.form.new-user": "New user? Click here to register.", + "login.form.new-user": "Jauns lietotājs? Noklikšķiniet šeit, lai reģistrētos.", + + // "login.form.or-divider": "or", + "login.form.or-divider": "vai", + + // "login.form.password": "Password", + "login.form.password": "Parole", + + // "login.form.shibboleth": "Log in with Shibboleth", + "login.form.shibboleth": "Pieslēgties ar Shibboleth", + + // "login.form.submit": "Log in", + "login.form.submit": "Pieslēgties", + + // "login.title": "Login", + "login.title": "Pierakstīties", + + // "login.breadcrumbs": "Login", + "login.breadcrumbs": "Pierakstīties", + + + + // "logout.form.header": "Log out from DSpace", + "logout.form.header": "Izrakstīties no DSpace", + + // "logout.form.submit": "Log out", + "logout.form.submit": "Izrakstīties", + + // "logout.title": "Logout", + "logout.title": "Izrakstīties", + + + + // "menu.header.admin": "Admin", + "menu.header.admin": "Administrators", + + // "menu.header.image.logo": "Repository logo", + "menu.header.image.logo": "Repozitorijas logotips", + + + + // "menu.section.access_control": "Access Control", + "menu.section.access_control": "Piekļuves kontrole", + + // "menu.section.access_control_authorizations": "Authorizations", + "menu.section.access_control_authorizations": "Pilnvaras", + + // "menu.section.access_control_groups": "Groups", + "menu.section.access_control_groups": "Grupas", + + // "menu.section.access_control_people": "People", + "menu.section.access_control_people": "Personas", + + + + // "menu.section.admin_search": "Admin Search", + "menu.section.admin_search": "Administratora Meklēšana", + + + + // "menu.section.browse_community": "This Community", + "menu.section.browse_community": "Šī kategorija", + + // "menu.section.browse_community_by_author": "Pēc Autora", + "menu.section.browse_community_by_author": "Pēc Autora", + + // "menu.section.browse_community_by_issue_date": "By Issue Date", + "menu.section.browse_community_by_issue_date": "Pēc Izdošanas Datuma", + + // "menu.section.browse_community_by_title": "By Title", + "menu.section.browse_community_by_title": "Pēc Nosaukuma", + + // "menu.section.browse_global": "All of DSpace", + "menu.section.browse_global": "Viss no DSpace", + + // "menu.section.browse_global_by_author": "By Author", + "menu.section.browse_global_by_author": "Pēc Autora", + + // "menu.section.browse_global_by_dateissued": "By Issue Date", + "menu.section.browse_global_by_dateissued": "Pēc Izdošanas Datuma", + + // "menu.section.browse_global_by_subject": "By Subject", + "menu.section.browse_global_by_subject": "Pēc Priekšmeta", + + // "menu.section.browse_global_by_title": "By Title", + "menu.section.browse_global_by_title": "Pēc Nosaukuma", + + // "menu.section.browse_global_communities_and_collections": "Communities & Collections", + "menu.section.browse_global_communities_and_collections": "Kategorijas & Kolekcijas", + + + + // "menu.section.control_panel": "Control Panel", + "menu.section.control_panel": "Vadības Panelis", + + // "menu.section.curation_task": "Curation Task", + "menu.section.curation_task": "Kuratora Uzdevums", + + + + // "menu.section.edit": "Edit", + "menu.section.edit": "Rediģēt", + + // "menu.section.edit_collection": "Collection", + "menu.section.edit_collection": "Kolekcija", + + // "menu.section.edit_community": "Community", + "menu.section.edit_community": "Kategorija", + + // "menu.section.edit_item": "Item", + "menu.section.edit_item": "Materiāls", + + + + // "menu.section.export": "Export", + "menu.section.export": "Eksportēt", + + // "menu.section.export_collection": "Collection", + "menu.section.export_collection": "Kolekcija", + + // "menu.section.export_community": "Community", + "menu.section.export_community": "Kategorija", + + // "menu.section.export_item": "Item", + "menu.section.export_item": "Materiāls", + + // "menu.section.export_metadata": "Metadata", + "menu.section.export_metadata": "Metadati", + + + + // "menu.section.icon.access_control": "Access Control menu section", + "menu.section.icon.access_control": "Piekļuves kontroles izvēlnes sadaļa", + + // "menu.section.icon.admin_search": "Admin search menu section", + "menu.section.icon.admin_search": "Administratoru meklēšanas izvēlnes sadaļa", + + // "menu.section.icon.control_panel": "Control Panel menu section", + "menu.section.icon.control_panel": "Vadības paneļa izvēlnes sadaļa", + + // "menu.section.icon.curation_task": "Curation Task menu section", + "menu.section.icon.curation_task": "Kuratora uzdevumu izvēlnes sadaļa", + + // "menu.section.icon.edit": "Edit menu section", + "menu.section.icon.edit": "Labot izvēlnes sadaļu", + + // "menu.section.icon.export": "Export menu section", + "menu.section.icon.export": "Eksportēt izvēlnes sadaļu", + + // "menu.section.icon.find": "Find menu section", + "menu.section.icon.find": "Atrast izvēlnes sadaļu", + + // "menu.section.icon.import": "Import menu section", + "menu.section.icon.import": "Importēt izvēlnes sadaļa", + + // "menu.section.icon.new": "New menu section", + "menu.section.icon.new": "Jauna izvēlnes sadaļa", + + // "menu.section.icon.pin": "Pin sidebar", + "menu.section.icon.pin": "Piespraust sānjoslu", + + // "menu.section.icon.registries": "Registries menu section", + "menu.section.icon.registries": "Reģistru izvēlnes sadaļa", + + // "menu.section.icon.statistics_task": "Statistics Task menu section", + "menu.section.icon.statistics_task": "Statistkas uzdevumu izvēlnes sadaļa", + + // "menu.section.icon.unpin": "Unpin sidebar", + "menu.section.icon.unpin": "Atspraust sānjoslu", + + + + // "menu.section.import": "Import", + "menu.section.import": "Importēt", + + // "menu.section.import_batch": "Batch Import (ZIP)", + "menu.section.import_batch": "Importēt (ZIP)", + + // "menu.section.import_metadata": "Metadata", + "menu.section.import_metadata": "Metadati", + + + + // "menu.section.new": "New", + "menu.section.new": "Jauns", + + // "menu.section.new_collection": "Collection", + "menu.section.new_collection": "Kolkecija", + + // "menu.section.new_community": "Community", + "menu.section.new_community": "Kategorija", + + // "menu.section.new_item": "Item", + "menu.section.new_item": "Materiāls", + + // "menu.section.new_item_version": "Item Version", + "menu.section.new_item_version": "Materiāla Versija", + + + + // "menu.section.pin": "Pin sidebar", + "menu.section.pin": "Piespraust sānjoslu", + + // "menu.section.unpin": "Unpin sidebar", + "menu.section.unpin": "Atspraust sānjoslu", + + + + // "menu.section.registries": "Registries", + "menu.section.registries": "Reģistri", + + // "menu.section.registries_format": "Format", + "menu.section.registries_format": "Formāts", + + // "menu.section.registries_metadata": "Metadata", + "menu.section.registries_metadata": "Metadati", + + + + // "menu.section.statistics": "Statistics", + "menu.section.statistics": "Statistika", + + // "menu.section.statistics_task": "Statistics Task", + "menu.section.statistics_task": "Statistikas Uzdevumi", + + + + // "menu.section.toggle.access_control": "Toggle Access Control section", + "menu.section.toggle.access_control": "Pārslēgt Piekļuvas Kontronles sadaļu", + + // "menu.section.toggle.control_panel": "Toggle Control Panel section", + "menu.section.toggle.control_panel": "Pārslēgt Vadības Paneļa sadaļu", + + // "menu.section.toggle.curation_task": "Toggle Curation Task section", + "menu.section.toggle.curation_task": "Pārslēgt Kuratora Uzdevumu sadaļu", + + // "menu.section.toggle.edit": "Toggle Edit section", + "menu.section.toggle.edit": "Pārslēgt Rediģēt sadaļu", + + // "menu.section.toggle.export": "Toggle Export section", + "menu.section.toggle.export": "Pārslēgt Eksportēt sadaļu", + + // "menu.section.toggle.find": "Toggle Find section", + "menu.section.toggle.find": "Pārslēgt Meklēt sadaļu", + + // "menu.section.toggle.import": "Toggle Import section", + "menu.section.toggle.import": "Pārslēgt Importēt sadaļu", + + // "menu.section.toggle.new": "Toggle New section", + "menu.section.toggle.new": "Pārslēgt Jauns sadaļu", + + // "menu.section.toggle.registries": "Toggle Registries section", + "menu.section.toggle.registries": "Pārslēgt Reģistru sadaļu", + + // "menu.section.toggle.statistics_task": "Toggle Statistics Task section", + "menu.section.toggle.statistics_task": "Pārslēgt Statistikas Uzdevumu sadaļu", + + + + // "mydspace.description": "", + "mydspace.description": "", + + // "mydspace.general.text-here": "HERE", + "mydspace.general.text-here": "ŠEIT", + + // "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", + "mydspace.messages.controller-help": "Atlasiet šo opciju, lai nosūtītu materiāla objekta iesniedzējam.", + + // "mydspace.messages.description-placeholder": "Insert your message here...", + "mydspace.messages.description-placeholder": "Ievietojiet savu ziņojumu šeit ...", + + // "mydspace.messages.hide-msg": "Hide message", + "mydspace.messages.hide-msg": "Paslēpt ziņojumu", + + // "mydspace.messages.mark-as-read": "Mark as read", + "mydspace.messages.mark-as-read": "Atzīmēt kā lasītu", + + // "mydspace.messages.mark-as-unread": "Mark as unread", + "mydspace.messages.mark-as-unread": "Atzīmēt kā nelasītu", + + // "mydspace.messages.no-content": "No content.", + "mydspace.messages.no-content": "Nav satura.", + + // "mydspace.messages.no-messages": "No messages yet.", + "mydspace.messages.no-messages": "Nav paziņojumu.", + + // "mydspace.messages.send-btn": "Send", + "mydspace.messages.send-btn": "Sūtīt", + + // "mydspace.messages.show-msg": "Show message", + "mydspace.messages.show-msg": "Parādīt ziņu", + + // "mydspace.messages.subject-placeholder": "Subject...", + "mydspace.messages.subject-placeholder": "Priekšmets...", + + // "mydspace.messages.submitter-help": "Select this option to send a message to controller.", + "mydspace.messages.submitter-help": "Atlasiet šo opciju, lai nosūtītu ziņojumu kontrolierim.", + + // "mydspace.messages.title": "Messages", + "mydspace.messages.title": "Vēstules", + + // "mydspace.messages.to": "To", + "mydspace.messages.to": "Uz", + + // "mydspace.new-submission": "New submission", + "mydspace.new-submission": "Jauns iesniegums", + + // "mydspace.results.head": "Your submissions", + "mydspace.results.head": "Jūsu iesniegumi", + + // "mydspace.results.no-abstract": "No Abstract", + "mydspace.results.no-abstract": "Nav Kopsavilkums", + + // "mydspace.results.no-authors": "No Authors", + "mydspace.results.no-authors": "Nav Autoru", + + // "mydspace.results.no-collections": "No Collections", + "mydspace.results.no-collections": "Nav Kolekcijas", + + // "mydspace.results.no-date": "No Date", + "mydspace.results.no-date": "Nav Datuma", + + // "mydspace.results.no-files": "No Files", + "mydspace.results.no-files": "Nav Failu", + + // "mydspace.results.no-results": "There were no items to show", + "mydspace.results.no-results": "Nav materiālu, ko parādīt", + + // "mydspace.results.no-title": "No title", + "mydspace.results.no-title": "Nav Nosaukuma", + + // "mydspace.results.no-uri": "No Uri", + "mydspace.results.no-uri": "Nav Uri", + + // "mydspace.show.workflow": "All tasks", + "mydspace.show.workflow": "Visi uzdevumi", + + // "mydspace.show.workspace": "Your Submissions", + "mydspace.show.workspace": "Jūsu Iesniegumi", + + // "mydspace.status.archived": "Archived", + "mydspace.status.archived": "Arhivēts", + + // "mydspace.status.validation": "Validation", + "mydspace.status.validation": "Validācija", + + // "mydspace.status.waiting-for-controller": "Waiting for controller", + "mydspace.status.waiting-for-controller": "Gaida kontrolieri", + + // "mydspace.status.workflow": "Workflow", + "mydspace.status.workflow": "Darba plūsma", + + // "mydspace.status.workspace": "Workspace", + "mydspace.status.workspace": "Darbavieta", + + // "mydspace.title": "MyDSpace", + "mydspace.title": "Mans DSpace", + + // "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", + "mydspace.upload.upload-failed": "Veidojot jaunu darbvietu, radās kļūda. Lūdzu pārbaudiet augšupielādēto saturu pirms mēģiniet vēlreiz.", + + // "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", + "mydspace.upload.upload-multiple-successful": "{{qty}} izveidoti jauni darbvietas materiāli.", + + // "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", + "mydspace.upload.upload-successful": "Ir izveidots jauns darbvietas materiāls. Noklikšķiniet {{here}}, lai to rediģētu.", + + // "mydspace.view-btn": "View", + "mydspace.view-btn": "Skatīt", + + + + // "nav.browse.header": "All of DSpace", + "nav.browse.header": "Viss no DSpace", + + // "nav.community-browse.header": "By Community", + "nav.community-browse.header": "Pēc Kategorijas", + + // "nav.language": "Language switch", + "nav.language": "Valodas maiņa", + + // "nav.login": "Log In", + "nav.login": "Pieslēgties", + + // "nav.logout": "Log Out", + "nav.logout": "Izrakstīties", + + // "nav.mydspace": "MyDSpace", + "nav.mydspace": "Mans DSpace", + + // "nav.profile": "Profile", + "nav.profile": "Profils", + + // "nav.search": "Search", + "nav.search": "Meklēt", + + // "nav.statistics.header": "Statistics", + "nav.statistics.header": "Statistika", + + + + // "orgunit.listelement.badge": "Organizational Unit", + "orgunit.listelement.badge": "Struktūrvienība", + + // "orgunit.page.city": "City", + "orgunit.page.city": "Pilsēta", + + // "orgunit.page.country": "Country", + "orgunit.page.country": "Valsts", + + // "orgunit.page.dateestablished": "Date established", + "orgunit.page.dateestablished": "Dibināšanas datums", + + // "orgunit.page.description": "Description", + "orgunit.page.description": "Apraksts", + + // "orgunit.page.id": "ID", + "orgunit.page.id": "ID", + + // "orgunit.page.titleprefix": "Organizational Unit: ", + "orgunit.page.titleprefix": "Struktūrvienība: ", + + + + // "pagination.results-per-page": "Results Per Page", + "pagination.results-per-page": "Rezultāti vienā lapā", + + // "pagination.showing.detail": "{{ range }} of {{ total }}", + "pagination.showing.detail": "{{ range }} no {{ total }}", + + // "pagination.showing.label": "Now showing ", + "pagination.showing.label": "Tagad rāda ", + + // "pagination.sort-direction": "Sort Options", + "pagination.sort-direction": "Kārtošanas iespējas", + + + + // "person.listelement.badge": "Person", + "person.listelement.badge": "Persona", + + // "person.page.birthdate": "Birth Date", + "person.page.birthdate": "Dzimšanas datums", + + // "person.page.email": "Email Address", + "person.page.email": "E-pasta adrese", + + // "person.page.firstname": "First Name", + "person.page.firstname": "Vārds", + + // "person.page.jobtitle": "Job Title", + "person.page.jobtitle": "Ieņemamais amats", + + // "person.page.lastname": "Last Name", + "person.page.lastname": "Uzvārds", + + // "person.page.link.full": "Show all metadata", + "person.page.link.full": "Parādīt visus metadatus", + + // "person.page.orcid": "ORCID", + "person.page.orcid": "ORCID", + + // "person.page.staffid": "Staff ID", + "person.page.staffid": "Personāla ID", + + // "person.page.titleprefix": "Person: ", + "person.page.titleprefix": "Persona: ", + + // "person.search.results.head": "Person Search Results", + "person.search.results.head": "Personas meklēšanas rezultāti", + + // "person.search.title": "DSpace Angular :: Person Search", + "person.search.title": "DSpace Angular :: Personas Meklēšana", + + + + // "profile.breadcrumbs": "Update Profile", + "profile.breadcrumbs": "Atjaunot Profilu", + + // "profile.card.identify": "Identify", + "profile.card.identify": "Identificēt", + + // "profile.card.security": "Security", + "profile.card.security": "Drošība", + + // "profile.form.submit": "Update Profile", + "profile.form.submit": "Atjaunot Profilu", + + // "profile.groups.head": "Authorization groups you belong to", + "profile.groups.head": "Autorizācijas grupas, kurām jūs piederat", + + // "profile.head": "Update Profile", + "profile.head": "Atjaunot Profilu", + + // "profile.metadata.form.error.firstname.required": "First Name is required", + "profile.metadata.form.error.firstname.required": "Vārds ir nepieciešams", + + // "profile.metadata.form.error.lastname.required": "Last Name is required", + "profile.metadata.form.error.lastname.required": "Uzvārds ir nepieciešams", + + // "profile.metadata.form.label.email": "Email Address", + "profile.metadata.form.label.email": "E-pasta adrese", + + // "profile.metadata.form.label.firstname": "First Name", + "profile.metadata.form.label.firstname": "Vārds", + + // "profile.metadata.form.label.language": "Language", + "profile.metadata.form.label.language": "Valoda", + + // "profile.metadata.form.label.lastname": "Last Name", + "profile.metadata.form.label.lastname": "Uzvārds", + + // "profile.metadata.form.label.phone": "Contact Telephone", + "profile.metadata.form.label.phone": "Kontakttālrunis", + + // "profile.metadata.form.notifications.success.content": "Your changes to the profile were saved.", + "profile.metadata.form.notifications.success.content": "Profila izmaiņas tika saglabātas.", + + // "profile.metadata.form.notifications.success.title": "Profile saved", + "profile.metadata.form.notifications.success.title": "Profils saglabāts", + + // "profile.notifications.warning.no-changes.content": "No changes were made to the Profile.", + "profile.notifications.warning.no-changes.content": "Profilā izmaiņas netika veiktas.", + + // "profile.notifications.warning.no-changes.title": "No changes", + "profile.notifications.warning.no-changes.title": "Bez izmaiņām", + + // "profile.security.form.error.matching-passwords": "The passwords do not match.", + "profile.security.form.error.matching-passwords": "Paroles nesakrīt.", + + // "profile.security.form.error.password-length": "The password should be at least 6 characters long.", + "profile.security.form.error.password-length": "Parolei jābūt vismaz 6 rakstzīmju garai.", + + // "profile.security.form.info": "Optionally, you can enter a new password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", + "profile.security.form.info": "Pēc izvēles zemāk esošajā lodziņā varat ievadīt jaunu paroli un apstiprināt to, atkārtoti ierakstot to otrajā lodziņā. Tam jābūt vismaz sešu rakstzīmju garam.", + + // "profile.security.form.label.password": "Password", + "profile.security.form.label.password": "Parole", + + // "profile.security.form.label.passwordrepeat": "Retype to confirm", + "profile.security.form.label.passwordrepeat": "Atkārtojiet, lai apstiprinātu", + + // "profile.security.form.notifications.success.content": "Your changes to the password were saved.", + "profile.security.form.notifications.success.content": "Jūsu paroles izmaiņas tika saglabātas.", + + // "profile.security.form.notifications.success.title": "Password saved", + "profile.security.form.notifications.success.title": "Parole saglabāta", + + // "profile.security.form.notifications.error.title": "Error changing passwords", + "profile.security.form.notifications.error.title": "Kļūda mainot paroles", + + // "profile.security.form.notifications.error.not-long-enough": "The password has to be at least 6 characters long.", + "profile.security.form.notifications.error.not-long-enough": "Parolei jābūt vismaz 6 rakstzīmju garai.", + + // "profile.security.form.notifications.error.not-same": "The provided passwords are not the same.", + "profile.security.form.notifications.error.not-same": "Norādītās paroles nav vienādas.", + + // "profile.title": "Update Profile", + "profile.title": "Atjaunot profilu", + + + + // "project.listelement.badge": "Research Project", + "project.listelement.badge": "Izpētes projekts", + + // "project.page.contributor": "Contributors", + "project.page.contributor": "Līdzautori", + + // "project.page.description": "Description", + "project.page.description": "Apraksts", + + // "project.page.expectedcompletion": "Expected Completion", + "project.page.expectedcompletion": "Sagaidāmais pabeigšanas datums", + + // "project.page.funder": "Funders", + "project.page.funder": "Izveidotāji", + + // "project.page.id": "ID", + "project.page.id": "ID", + + // "project.page.keyword": "Keywords", + "project.page.keyword": "Atslēgas vārdi", + + // "project.page.status": "Status", + "project.page.status": "Status", + + // "project.page.titleprefix": "Research Project: ", + "project.page.titleprefix": "Izpētes projekts: ", + + // "project.search.results.head": "Project Search Results", + "project.search.results.head": "Projekta meklēšanas rezultāti", + + + + // "publication.listelement.badge": "Publication", + "publication.listelement.badge": "Publikācija", + + // "publication.page.description": "Description", + "publication.page.description": "Apraksts", + + // "publication.page.journal-issn": "Journal ISSN", + "publication.page.journal-issn": "Žurnāla ISSN", + + // "publication.page.journal-title": "Journal Title", + "publication.page.journal-title": "Žurnāla Nosaukums", + + // "publication.page.publisher": "Publisher", + "publication.page.publisher": "Izdevējs", + + // "publication.page.titleprefix": "Publication: ", + "publication.page.titleprefix": "Publikācija: ", + + // "publication.page.volume-title": "Volume Title", + "publication.page.volume-title": "Sējuma Nosaukums", + + // "publication.search.results.head": "Publication Search Results", + "publication.search.results.head": "Publikāciju meklēšanas rezultāti", + + // "publication.search.title": "DSpace Angular :: Publication Search", + "publication.search.title": "DSpace Angular :: Publikācijas meklēšana", + + + + // "relationships.isAuthorOf": "Authors", + "relationships.isAuthorOf": "Autori", + + // "relationships.isIssueOf": "Journal Issues", + "relationships.isIssueOf": "Žurnāla Izdevumi", + + // "relationships.isJournalIssueOf": "Journal Issue", + "relationships.isJournalIssueOf": "Žurnāla Izdevums", + + // "relationships.isJournalOf": "Journals", + "relationships.isJournalOf": "Žurnāli", + + // "relationships.isOrgUnitOf": "Organizational Units", + "relationships.isOrgUnitOf": "Struktūrvienības", + + // "relationships.isPersonOf": "Authors", + "relationships.isPersonOf": "Autori", + + // "relationships.isProjectOf": "Research Projects", + "relationships.isProjectOf": "Pētniecības Projekti", + + // "relationships.isPublicationOf": "Publications", + "relationships.isPublicationOf": "Publikācijas", + + // "relationships.isPublicationOfJournalIssue": "Articles", + "relationships.isPublicationOfJournalIssue": "Raksti", + + // "relationships.isSingleJournalOf": "Journal", + "relationships.isSingleJournalOf": "Žurnāls", + + // "relationships.isSingleVolumeOf": "Journal Volume", + "relationships.isSingleVolumeOf": "Žurnāla Sējums", + + // "relationships.isVolumeOf": "Journal Volumes", + "relationships.isVolumeOf": "Žurnāla Sējums", + + // "relationships.isContributorOf": "Contributors", + "relationships.isContributorOf": "Līdzautori", + + + + // "search.description": "", + "search.description": "", + + // "search.switch-configuration.title": "Show", + "search.switch-configuration.title": "Rādīt", + + // "search.title": "DSpace Angular :: Search", + "search.title": "DSpace Angular :: Meklēt", + + // "search.breadcrumbs": "Search", + "search.breadcrumbs": "Meklēt", + + + // "search.filters.applied.f.author": "Author", + "search.filters.applied.f.author": "Autors", + + // "search.filters.applied.f.dateIssued.max": "End date", + "search.filters.applied.f.dateIssued.max": "Beigu datums", + + // "search.filters.applied.f.dateIssued.min": "Start date", + "search.filters.applied.f.dateIssued.min": "Sākuma datums", + + // "search.filters.applied.f.dateSubmitted": "Date submitted", + "search.filters.applied.f.dateSubmitted": "Iesniegšanas datums", + + // "search.filters.applied.f.discoverable": "Private", + "search.filters.applied.f.discoverable": "Privāts", + + // "search.filters.applied.f.entityType": "Item Type", + "search.filters.applied.f.entityType": "Materiāla tips", + + // "search.filters.applied.f.has_content_in_original_bundle": "Has files", + "search.filters.applied.f.has_content_in_original_bundle": "Ir faili", + + // "search.filters.applied.f.itemtype": "Type", + "search.filters.applied.f.itemtype": "Tips", + + // "search.filters.applied.f.namedresourcetype": "Status", + "search.filters.applied.f.namedresourcetype": "Status", + + // "search.filters.applied.f.subject": "Subject", + "search.filters.applied.f.subject": "Priekšmets", + + // "search.filters.applied.f.submitter": "Submitter", + "search.filters.applied.f.submitter": "Iesniedzējs", + + // "search.filters.applied.f.jobTitle": "Job Title", + "search.filters.applied.f.jobTitle": "Ieņemamais Amats", + + // "search.filters.applied.f.birthDate.max": "End birth date", + "search.filters.applied.f.birthDate.max": "Dzimšanas beigu datums", + + // "search.filters.applied.f.birthDate.min": "Start birth date", + "search.filters.applied.f.birthDate.min": "Dzimšanas sākuma datums", + + // "search.filters.applied.f.withdrawn": "Withdrawn", + "search.filters.applied.f.withdrawn": "Atsaukts", + + + + // "search.filters.filter.author.head": "Author", + "search.filters.filter.author.head": "Autors", + + // "search.filters.filter.author.placeholder": "Author name", + "search.filters.filter.author.placeholder": "Autora vārds", + + // "search.filters.filter.birthDate.head": "Birth Date", + "search.filters.filter.birthDate.head": "Dzimšanas datums", + + // "search.filters.filter.birthDate.placeholder": "Birth Date", + "search.filters.filter.birthDate.placeholder": "Dzimšanas datums", + + // "search.filters.filter.creativeDatePublished.head": "Date Published", + "search.filters.filter.creativeDatePublished.head": "Publicēšanas datums", + + // "search.filters.filter.creativeDatePublished.placeholder": "Date Published", + "search.filters.filter.creativeDatePublished.placeholder": "Publicēšanas datums", + + // "search.filters.filter.creativeWorkEditor.head": "Editor", + "search.filters.filter.creativeWorkEditor.head": "Redaktors", + + // "search.filters.filter.creativeWorkEditor.placeholder": "Editor", + "search.filters.filter.creativeWorkEditor.placeholder": "Redaktors", + + // "search.filters.filter.creativeWorkKeywords.head": "Subject", + "search.filters.filter.creativeWorkKeywords.head": "Priekšmets", + + // "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", + "search.filters.filter.creativeWorkKeywords.placeholder": "Priekšmets", + + // "search.filters.filter.creativeWorkPublisher.head": "Publisher", + "search.filters.filter.creativeWorkPublisher.head": "Izdevējs", + + // "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", + "search.filters.filter.creativeWorkPublisher.placeholder": "Izdevējs", + + // "search.filters.filter.dateIssued.head": "Date", + "search.filters.filter.dateIssued.head": "Datums", + + // "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", + "search.filters.filter.dateIssued.max.placeholder": "Minimālais datums", + + // "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", + "search.filters.filter.dateIssued.min.placeholder": "Maksimālais datums", + + // "search.filters.filter.dateSubmitted.head": "Date submitted", + "search.filters.filter.dateSubmitted.head": "Iesniegšanas datums", + + // "search.filters.filter.dateSubmitted.placeholder": "Date submitted", + "search.filters.filter.dateSubmitted.placeholder": "Iesniegšanas datums", + + // "search.filters.filter.discoverable.head": "Private", + "search.filters.filter.discoverable.head": "Privāts", + + // "search.filters.filter.withdrawn.head": "Withdrawn", + "search.filters.filter.withdrawn.head": "Atsaukts", + + // "search.filters.filter.entityType.head": "Item Type", + "search.filters.filter.entityType.head": "Materiāla tips", + + // "search.filters.filter.entityType.placeholder": "Item Type", + "search.filters.filter.entityType.placeholder": "Materiāla tips", + + // "search.filters.filter.has_content_in_original_bundle.head": "Has files", + "search.filters.filter.has_content_in_original_bundle.head": "Ir faili", + + // "search.filters.filter.itemtype.head": "Type", + "search.filters.filter.itemtype.head": "Tips", + + // "search.filters.filter.itemtype.placeholder": "Type", + "search.filters.filter.itemtype.placeholder": "Tips", + + // "search.filters.filter.jobTitle.head": "Job Title", + "search.filters.filter.jobTitle.head": "Ieņemamais Amats", + + // "search.filters.filter.jobTitle.placeholder": "Job Title", + "search.filters.filter.jobTitle.placeholder": "Ieņemamais Amats", + + // "search.filters.filter.knowsLanguage.head": "Known language", + "search.filters.filter.knowsLanguage.head": "Pārvalda valodu", + + // "search.filters.filter.knowsLanguage.placeholder": "Known language", + "search.filters.filter.knowsLanguage.placeholder": "Pārvalda valodu", + + // "search.filters.filter.namedresourcetype.head": "Status", + "search.filters.filter.namedresourcetype.head": "Status", + + // "search.filters.filter.namedresourcetype.placeholder": "Status", + "search.filters.filter.namedresourcetype.placeholder": "Status", + + // "search.filters.filter.objectpeople.head": "People", + "search.filters.filter.objectpeople.head": "Personas", + + // "search.filters.filter.objectpeople.placeholder": "People", + "search.filters.filter.objectpeople.placeholder": "Personas", + + // "search.filters.filter.organizationAddressCountry.head": "Country", + "search.filters.filter.organizationAddressCountry.head": "Valsts", + + // "search.filters.filter.organizationAddressCountry.placeholder": "Country", + "search.filters.filter.organizationAddressCountry.placeholder": "Valsts", + + // "search.filters.filter.organizationAddressLocality.head": "City", + "search.filters.filter.organizationAddressLocality.head": "Pilsēta", + + // "search.filters.filter.organizationAddressLocality.placeholder": "City", + "search.filters.filter.organizationAddressLocality.placeholder": "Pilsēta", + + // "search.filters.filter.organizationFoundingDate.head": "Date Founded", + "search.filters.filter.organizationFoundingDate.head": "Dibināšanas datums", + + // "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", + "search.filters.filter.organizationFoundingDate.placeholder": "Dibināšanas datums", + + // "search.filters.filter.scope.head": "Scope", + "search.filters.filter.scope.head": "Joma", + + // "search.filters.filter.scope.placeholder": "Scope filter", + "search.filters.filter.scope.placeholder": "Jomas filtrs", + + // "search.filters.filter.show-less": "Collapse", + "search.filters.filter.show-less": "Sakļaut", + + // "search.filters.filter.show-more": "Show more", + "search.filters.filter.show-more": "Rādīt vairāk", + + // "search.filters.filter.subject.head": "Subject", + "search.filters.filter.subject.head": "Priekšmets", + + // "search.filters.filter.subject.placeholder": "Subject", + "search.filters.filter.subject.placeholder": "Priekšmets", + + // "search.filters.filter.submitter.head": "Submitter", + "search.filters.filter.submitter.head": "Iesniedzējs", + + // "search.filters.filter.submitter.placeholder": "Submitter", + "search.filters.filter.submitter.placeholder": "Iesniedzējs", + + + + // "search.filters.entityType.JournalIssue": "Journal Issue", + "search.filters.entityType.JournalIssue": "Žurnāla Izdevums", + + // "search.filters.entityType.JournalVolume": "Journal Volume", + "search.filters.entityType.JournalVolume": "Žurnāla Sējum", + + // "search.filters.entityType.OrgUnit": "Organizational Unit", + "search.filters.entityType.OrgUnit": "Struktūrvienība", + + // "search.filters.has_content_in_original_bundle.true": "Yes", + "search.filters.has_content_in_original_bundle.true": "Jā", + + // "search.filters.has_content_in_original_bundle.false": "No", + "search.filters.has_content_in_original_bundle.false": "Nē", + + // "search.filters.discoverable.true": "No", + "search.filters.discoverable.true": "Nē", + + // "search.filters.discoverable.false": "Yes", + "search.filters.discoverable.false": "Jā", + + // "search.filters.withdrawn.true": "Yes", + "search.filters.withdrawn.true": "Jā", + + // "search.filters.withdrawn.false": "No", + "search.filters.withdrawn.false": "Nē", + + + // "search.filters.head": "Filters", + "search.filters.head": "Filtri", + + // "search.filters.reset": "Reset filters", + "search.filters.reset": "Atiestatīt filtrus", + + + + // "search.form.search": "Search", + "search.form.search": "Meklēt", + + // "search.form.search_dspace": "Search DSpace", + "search.form.search_dspace": "Meklēt DSpace", + + // "search.form.search_mydspace": "Search MyDSpace", + "search.form.search_mydspace": "Meklēt manā DSpace", + + + + // "search.results.head": "Search Results", + "search.results.head": "Meklēšanas rezultāti", + + // "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", + "search.results.no-results": "Rezultāti netika atrasti. Vai jums ir grūtības atrast meklēto? Mēģiniet ievietot", + + // "search.results.no-results-link": "quotes around it", + "search.results.no-results-link": "pēdiņas ap to", + + // "search.results.empty": "Your search returned no results.", + "search.results.empty": "Rezultāti netika atrasti.", + + + + // "search.sidebar.close": "Back to results", + "search.sidebar.close": "Atpakaļ pie rezultātiem", + + // "search.sidebar.filters.title": "Filters", + "search.sidebar.filters.title": "Filtri", + + // "search.sidebar.open": "Search Tools", + "search.sidebar.open": "Meklēšanas Rīki", + + // "search.sidebar.results": "results", + "search.sidebar.results": "rezultāti", + + // "search.sidebar.settings.rpp": "Results per page", + "search.sidebar.settings.rpp": "Rezultāti vienā lapā", + + // "search.sidebar.settings.sort-by": "Sort By", + "search.sidebar.settings.sort-by": "Kārtot Pēc", + + // "search.sidebar.settings.title": "Settings", + "search.sidebar.settings.title": "Iestatījumi", + + + + // "search.view-switch.show-detail": "Show detail", + "search.view-switch.show-detail": "Attēlot detaļas", + + // "search.view-switch.show-grid": "Show as grid", + "search.view-switch.show-grid": "Attēlot matricā", + + // "search.view-switch.show-list": "Show as list", + "search.view-switch.show-list": "Attēlot kā sarakstu", + + + + // "sorting.dc.title.ASC": "Title Ascending", + "sorting.dc.title.ASC": "Nosaukums augošā secībā", + + // "sorting.dc.title.DESC": "Title Descending", + "sorting.dc.title.DESC": "Nosaukums dilstošā secībā", + + // "sorting.score.DESC": "Relevance", + "sorting.score.DESC": "Atbilstība", + + + + // "submission.edit.title": "Edit Submission", + "submission.edit.title": "Rediģēt Iesniegumu", + + // "submission.general.cannot_submit": "You have not the privilege to make a new submission.", + "submission.general.cannot_submit": "Jums nav tiesību iesniegt jaunu iesniegumu.", + + // "submission.general.deposit": "Deposit", + "submission.general.deposit": "Ievietot", + + // "submission.general.discard.confirm.cancel": "Cancel", + "submission.general.discard.confirm.cancel": "Atcelt", + + // "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", + "submission.general.discard.confirm.info": "Šo darbību nevar atsaukt. Vai tu esat pārliecināts?", + + // "submission.general.discard.confirm.submit": "Yes, I'm sure", + "submission.general.discard.confirm.submit": "Jā, esmu pārliecināts", + + // "submission.general.discard.confirm.title": "Discard submission", + "submission.general.discard.confirm.title": "Atcelt ievietošanu", + + // "submission.general.discard.submit": "Discard", + "submission.general.discard.submit": "Atcelt", + + // "submission.general.save": "Save", + "submission.general.save": "Saglabāt", + + // "submission.general.save-later": "Save for later", + "submission.general.save-later": "Saglabāt vēlākam", + + + + // "submission.sections.describe.relationship-lookup.close": "Close", + "submission.sections.describe.relationship-lookup.close": "Aizvērt", + + // "submission.sections.describe.relationship-lookup.external-source.added": "Successfully added local entry to the selection", + "submission.sections.describe.relationship-lookup.external-source.added": "Lokālais ieraksts veiksmigi pievienots izvēlei", + + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Author": "Import remote author", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Author": "Importēt attālinātu autoru", + + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal": "Import remote journal", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal": "Importēt attālinātu žurnālu", + + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Issue": "Import remote journal issue", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Issue": "Importēt attālināta žurnāla izdevumu", + + // "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Volume": "Import remote journal volume", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Volume": "Importēt attālinātā žurnāla sējumu", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.title": "Import Remote Author", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.title": "Importēt Attālinātu Autoru", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.local-entity": "Successfully added local author to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.local-entity": "Lokālais autors ir veiksmīgi pievienots atlasei", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.new-entity": "Successfully imported and added external author to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.new-entity": "Veiksmīgi importēts un pievienots ārējais autors atlasē", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Authority", + "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Autoritāte", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.authority.new": "Import as a new local authority entry", + "submission.sections.describe.relationship-lookup.external-source.import-modal.authority.new": "Importēt kā jaunu lokālās autoritātes ierakstu", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Cancel", + "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Atcelt", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.collection": "Select a collection to import new entries to", + "submission.sections.describe.relationship-lookup.external-source.import-modal.collection": "Atlasiet kolekciju, kurā importēt jaunus ierakstus", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Entities", + "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Vienības", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.entities.new": "Import as a new local entity", + "submission.sections.describe.relationship-lookup.external-source.import-modal.entities.new": "Importēt kā jaunu vietējo vienību", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name", + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importē no LC Nosaukuma", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID", + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importē no ORCID", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal", + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importē no Sherpa Žurnāla", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaPublisher": "Importing from Sherpa Publisher", + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaPublisher": "Importēt no Sherpa Izdevēja", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Import", + "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Importēt", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.title": "Import Remote Journal", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.title": "Importēt Attālinātu Žurnālu", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.local-entity": "Successfully added local journal to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.local-entity": "Lokālais žurnāls veiksmīgi pievienots izlasei", + + // "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": "Veiksmīgi importēts un pievienots ārējais žurnāls atlasē", + + // "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": "Importēt Attālināta Žurnāla Izdevumu", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.local-entity": "Successfully added local journal issue to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.local-entity": "Lokālā žurnāla izdevums veiksmīgi pievienots izlasei", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.new-entity": "Successfully imported and added external journal issue to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.new-entity": "Veiksmīgi importēts un atlasē pievienots ārēja žurnāla izdevums", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.title": "Import Remote Journal Volume", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.title": "Importēt Attālinātā Žurnāla Sējumu", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.local-entity": "Successfully added local journal volume to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.local-entity": "Lokālā žurnāla sējums veiksmīgi pievienots atlasē", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.new-entity": "Successfully imported and added external journal volume to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.new-entity": "Veiksmīgi importēts un atlasē pievienots ārējā žurnāla sējums", + + // "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Select a local match:", + "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Atlasīt lokālo sakritību:", + + // "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all", + "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Atcelt izvēli", + + // "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page", + "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Atcelt izvēlēto lapu", + + // "submission.sections.describe.relationship-lookup.search-tab.loading": "Loading...", + "submission.sections.describe.relationship-lookup.search-tab.loading": "Notiek ielāde...", + + // "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Search query", + "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Meklēšanas vaicājums", + + // "submission.sections.describe.relationship-lookup.search-tab.search": "Go", + "submission.sections.describe.relationship-lookup.search-tab.search": "Izpildīt", + + // "submission.sections.describe.relationship-lookup.search-tab.select-all": "Select all", + "submission.sections.describe.relationship-lookup.search-tab.select-all": "Izvēlēties visus", + + // "submission.sections.describe.relationship-lookup.search-tab.select-page": "Select page", + "submission.sections.describe.relationship-lookup.search-tab.select-page": "Izvēlēties lapu", + + // "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", + "submission.sections.describe.relationship-lookup.selected": "Izvēlētie {{ size }} materiāli", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Vietējie Autori ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Vietējie Žurnāli ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Local Journal Issues ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Vietējie Žurnālu Izdevumi ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Local Journal Volumes ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Vietējie Žurnālu Sējumi ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Žurnāli ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Izdevēji ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Nosaukumi ({{ count }})", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Finansēšanas aģentūru meklēšana", + + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding": "Search for Funding", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding": "Finansējuma meklēšana", + + // "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", + "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Pašreiz Izvēlēti ({{ count }})", + + // "submission.sections.describe.relationship-lookup.title.Journal Issue": "Journal Issues", + "submission.sections.describe.relationship-lookup.title.Journal Issue": "Žurnāla Izdevumi", + + // "submission.sections.describe.relationship-lookup.title.Journal Volume": "Journal Volumes", + "submission.sections.describe.relationship-lookup.title.Journal Volume": "Žurnāla Sējumi", + + // "submission.sections.describe.relationship-lookup.title.Journal": "Journals", + "submission.sections.describe.relationship-lookup.title.Journal": "Žurnāls", + + // "submission.sections.describe.relationship-lookup.title.Author": "Authors", + "submission.sections.describe.relationship-lookup.title.Author": "Autors", + + // "submission.sections.describe.relationship-lookup.title.Funding Agency": "Funding Agency", + "submission.sections.describe.relationship-lookup.title.Funding Agency": "Finansējošā aģēntūra", + + // "submission.sections.describe.relationship-lookup.title.Funding": "Funding", + "submission.sections.describe.relationship-lookup.title.Funding": "Finansējums", + + // "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown", + "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Pārslēgšanas nolaižamā izvēlne", + + // "submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings", + "submission.sections.describe.relationship-lookup.selection-tab.settings": "Iestatījumi", + + // "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Your selection is currently empty.", + "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "ūsu atlase pašlaik ir tukša.", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.Author": "Selected Authors", + "submission.sections.describe.relationship-lookup.selection-tab.title.Author": "Izvēlētais Autors", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals", + "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Izvēlētais Žurnāls", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Volume": "Selected Journal Volume", + "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Volume": "Izvēlētā Žurnāla sējums", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Izvēlētais Izdevums", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Meklēšanas rezultāti", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Meklēšanas rezultāti", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Meklēšanas rezultāti", + + // "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Meklēšanas rezultāti", + + // "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.", + "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Vai vēlaties saglabāt \"{{ value }}\" kā vārda variantu šai personai, lai jūs un citi varētu to izmantot turpmākai iesniegšanai? Ja nē, jūs joprojām varat to izmantot šai iesniegšanai.", + + // "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant", + "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Saglabājiet jauna nosaukuma variantu", + + // "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Use only for this submission", + "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Izmantojiet tikai pašreizējai iesniegšanai", + + // "submission.sections.general.add-more": "Add more", + "submission.sections.general.add-more": "Pievienot vēl", + + // "submission.sections.general.collection": "Collection", + "submission.sections.general.collection": "Kolekcija", + + // "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", + "submission.sections.general.deposit_error_notice": "Iesniedzot materiālu, radās problēma. Lūdzu, vēlāk mēģiniet vēlreiz.", + + // "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", + "submission.sections.general.deposit_success_notice": "Iesniegums ir veiksmīgi iesniegts.", + + // "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", + "submission.sections.general.discard_error_notice": "Izmetot vienumu, radās kļūda. Lūdzu, vēlāk mēģiniet vēlreiz", + + // "submission.sections.general.discard_success_notice": "Submission discarded successfully.", + "submission.sections.general.discard_success_notice": "Iesniegums atmests.", + + // "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", + "submission.sections.general.metadata-extracted": "Jauni metadati ir iegūti un pievienoti {{sectionId}} sadaļā.", + + // "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", + "submission.sections.general.metadata-extracted-new-section": "Jauna {{sectionId}} sadaļa ir pievienota iesniegšanai.", + + // "submission.sections.general.no-collection": "No collection found", + "submission.sections.general.no-collection": "Kolekcijas nav atrastas", + + // "submission.sections.general.no-sections": "No options available", + "submission.sections.general.no-sections": "Opcijas nav pieejamas", + + // "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", + "submission.sections.general.save_error_notice": "Saglabājot materiālu, radās problēma. Lūdzu, vēlāk mēģiniet vēlreiz.", + + // "submission.sections.general.save_success_notice": "Submission saved successfully.", + "submission.sections.general.save_success_notice": "Iesniegums veiksmīgi saglabāts.", + + // "submission.sections.general.search-collection": "Search for a collection", + "submission.sections.general.search-collection": "Meklēt kolekciju", + + // "submission.sections.general.sections_not_valid": "There are incomplete sections.", + "submission.sections.general.sections_not_valid": "Ir nepilnīgas sadaļas.", + + + + // "submission.sections.submit.progressbar.cclicense": "Creative commons license", + "submission.sections.submit.progressbar.cclicense": "Creative commons licence", + + // "submission.sections.submit.progressbar.describe.recycle": "Recycle", + "submission.sections.submit.progressbar.describe.recycle": "Pārstrādāt", + + // "submission.sections.submit.progressbar.describe.stepcustom": "Describe", + "submission.sections.submit.progressbar.describe.stepcustom": "Aprakstiet", + + // "submission.sections.submit.progressbar.describe.stepone": "Describe", + "submission.sections.submit.progressbar.describe.stepone": "Aprakstiet", + + // "submission.sections.submit.progressbar.describe.steptwo": "Describe", + "submission.sections.submit.progressbar.describe.steptwo": "Aprakstiet", + + // "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", + "submission.sections.submit.progressbar.detect-duplicate": "Potenciālie dublikāti", + + // "submission.sections.submit.progressbar.license": "Deposit license", + "submission.sections.submit.progressbar.license": "Ievietot licenci", + + // "submission.sections.submit.progressbar.upload": "Upload files", + "submission.sections.submit.progressbar.upload": "Augšupielādēt failus", + + + + // "submission.sections.upload.delete.confirm.cancel": "Cancel", + "submission.sections.upload.delete.confirm.cancel": "Atcelt", + + // "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", + "submission.sections.upload.delete.confirm.info": "Šo darbību nevar atsaukt. Vai tu esi pārliecināts?", + + // "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", + "submission.sections.upload.delete.confirm.submit": "Jā, esmu pārliecināts", + + // "submission.sections.upload.delete.confirm.title": "Delete bitstream", + "submission.sections.upload.delete.confirm.title": "Dzēst bitu straumes", + + // "submission.sections.upload.delete.submit": "Delete", + "submission.sections.upload.delete.submit": "Dzēst", + + // "submission.sections.upload.drop-message": "Drop files to attach them to the item", + "submission.sections.upload.drop-message": "Ievietojiet failu, lai pievienotu materiālam", + + // "submission.sections.upload.form.access-condition-label": "Access condition type", + "submission.sections.upload.form.access-condition-label": "Piekļuves nosacījuma tips", + + // "submission.sections.upload.form.date-required": "Date is required.", + "submission.sections.upload.form.date-required": "Datums ir nepieciešams.", + + // "submission.sections.upload.form.from-label": "Access grant from", + "submission.sections.upload.form.from-label": "Iespēja izmantot no", + + // "submission.sections.upload.form.from-placeholder": "From", + "submission.sections.upload.form.from-placeholder": "No", + + // "submission.sections.upload.form.group-label": "Group", + "submission.sections.upload.form.group-label": "Grupa", + + // "submission.sections.upload.form.group-required": "Group is required.", + "submission.sections.upload.form.group-required": "Nepieciešama grupa.", + + // "submission.sections.upload.form.until-label": "Access grant until", + "submission.sections.upload.form.until-label": "Piekļuvas atļauja līdz", + + // "submission.sections.upload.form.until-placeholder": "Until", + "submission.sections.upload.form.until-placeholder": "Līdz", + + // "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", + "submission.sections.upload.header.policy.default.nolist": "Augšupielādētie faili kolekcijā {{collectionName}} būs pieejami atbilstoši šīm grupām:", + + // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", + "submission.sections.upload.header.policy.default.withlist": "Lūdzu, ņemiet vērā, ka augšupielādētie faili kolekcijā {{collectionName}} būs pieejami papildus tam, kas ir skaidri noteikts par atsevišķu failu, ar šādām grupām:", + + // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + "submission.sections.upload.info": "Šeit atradīsit visus failus, kas pašlaik atrodas materiālā. Varat atjaunināt failu metadatus un piekļuves nosacījumus vai augšupielādēt papildu failus, vienkārši ievelkot un atstājot tos visur lapā", + + // "submission.sections.upload.no-entry": "No", + "submission.sections.upload.no-entry": "Nē", + + // "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", + "submission.sections.upload.no-file-uploaded": "Vēl nav augšupielādēts neviens fails.", + + // "submission.sections.upload.save-metadata": "Save metadata", + "submission.sections.upload.save-metadata": "Saglabāt metadatus", + + // "submission.sections.upload.undo": "Cancel", + "submission.sections.upload.undo": "Atcelt", + + // "submission.sections.upload.upload-failed": "Upload failed", + "submission.sections.upload.upload-failed": "Augšupielāde neizdevās", + + // "submission.sections.upload.upload-successful": "Upload successful", + "submission.sections.upload.upload-successful": "Augšupielāde veiksmīga", + + + + // "submission.submit.title": "Submission", + "submission.submit.title": "Iesniegums", + + + + // "submission.workflow.generic.delete": "Delete", + "submission.workflow.generic.delete": "Dzēst", + + // "submission.workflow.generic.delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.", + "submission.workflow.generic.delete-help": "Ja vēlaties atmest šo materiālu izvēlieties \"Dzēst\". Pēc tam jums tiks lūgts to apstiprināt.", + + // "submission.workflow.generic.edit": "Edit", + "submission.workflow.generic.edit": "Rediģēt", + + // "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", + "submission.workflow.generic.edit-help": "Izvēlieties šo opciju, lai mainītu materiāla metadatus.", + + // "submission.workflow.generic.view": "View", + "submission.workflow.generic.view": "Skatīt", + + // "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", + "submission.workflow.generic.view-help": "Izvēlieties šo opciju, lai skatītu materiāla metadatus.", + + + + // "submission.workflow.tasks.claimed.approve": "Approve", + "submission.workflow.tasks.claimed.approve": "Apstiprināt", + + // "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", + "submission.workflow.tasks.claimed.approve_help": "Ja esat pārskatījis materiālu un tas ir piemērots iekļaušanai kolekcijā, atlasiet \"Apstiprināt\".", + + // "submission.workflow.tasks.claimed.edit": "Edit", + "submission.workflow.tasks.claimed.edit": "Rediģēt", + + // "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", + "submission.workflow.tasks.claimed.edit_help": "Izvēlieties šo opciju, lai mainītu materiāla metadatus.", + + // "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", + "submission.workflow.tasks.claimed.reject.reason.info": "Lūdzu, zemāk esošajā lodziņā ievadiet iesnieguma noraidīšanas iemeslu, norādot, vai iesniedzējs var novērst problēmu un atkārtoti iesniegt.", + + // "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", + "submission.workflow.tasks.claimed.reject.reason.placeholder": "Aprakstiet noraidījuma iemeslu", + + // "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", + "submission.workflow.tasks.claimed.reject.reason.submit": "Noraidīt materiālu", + + // "submission.workflow.tasks.claimed.reject.reason.title": "Reason", + "submission.workflow.tasks.claimed.reject.reason.title": "Iemesls", + + // "submission.workflow.tasks.claimed.reject.submit": "Reject", + "submission.workflow.tasks.claimed.reject.submit": "Noraidīt", + + // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", + "submission.workflow.tasks.claimed.reject_help": "Ja esat pārskatījis vienumu un secinājis, ka tas nav piemērots iekļaušanai kolekcijā, atlasiet \"Noraidīt\". Pēc tam jums tiks lūgts ievadīt ziņojumu, norādot, kāpēc materiāls nav piemērots, un vai iesniedzējam vajadzētu kaut ko mainīt un atkārtoti iesniegt.", + + // "submission.workflow.tasks.claimed.return": "Return to pool", + "submission.workflow.tasks.claimed.return": "Atgriezt kopnē", + + // "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", + "submission.workflow.tasks.claimed.return_help": "Atgrieziet uzdevumu kopnē, lai uzdevumu varētu veikt cits lietotājs.", + + + + // "submission.workflow.tasks.generic.error": "Error occurred during operation...", + "submission.workflow.tasks.generic.error": "Apstrādes laikā radās kļūda...", + + // "submission.workflow.tasks.generic.processing": "Processing...", + "submission.workflow.tasks.generic.processing": "Apstrāde...", + + // "submission.workflow.tasks.generic.submitter": "Submitter", + "submission.workflow.tasks.generic.submitter": "Iesniedzējs", + + // "submission.workflow.tasks.generic.success": "Operation successful", + "submission.workflow.tasks.generic.success": "Darbībā veiksmīga", + + + + // "submission.workflow.tasks.pool.claim": "Claim", + "submission.workflow.tasks.pool.claim": "Apstrādāt", + + // "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", + "submission.workflow.tasks.pool.claim_help": "Piešķirt šo uzdevumu sev.", + + // "submission.workflow.tasks.pool.hide-detail": "Hide detail", + "submission.workflow.tasks.pool.hide-detail": "Paslēpt detaļas", + + // "submission.workflow.tasks.pool.show-detail": "Show detail", + "submission.workflow.tasks.pool.show-detail": "Parādīt detaļas", + + + + // "title": "DSpace", + "title": "DSpace", + + + // "administrativeView.search.results.head": "Administrative Search", + "administrativeView.search.results.head": "Administratīvā meklēšana", + + + // "uploader.browse": "browse", + "uploader.browse": "pārlūkot", + + // "uploader.drag-message": "Drag & Drop your files here", + "uploader.drag-message": "Velciet failu šeit", + + // "uploader.or": ", or", + "uploader.or": ", vai", + + // "uploader.processing": "Processing", + "uploader.processing": "Datu apstrāde", + + // "uploader.queue-length": "Queue length", + "uploader.queue-length": "Rindas garums", + + // "virtual-metadata.delete-item.info": "Select the types for which you want to save the virtual metadata as real metadata", + "virtual-metadata.delete-item.info": "Izēlieties tipu, kam vēlaties saglabāt virtuālos metadatus kā reālus metadatus", + + // "virtual-metadata.delete-item.modal-head": "The virtual metadata of this relation", + "virtual-metadata.delete-item.modal-head": "Šīs saiknes virtuālie metadati", + + // "virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata", + "virtual-metadata.delete-relationship.modal-head": "Izēlieties materiālus, kam vēlaties saglabāt virtuālos metadatus kā reālus metadatus", + +} + diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 149ea92a5b..9dcac65363 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -152,6 +152,14 @@ export const environment: GlobalConfig = { code: 'pt', label: 'Português', active: true, + }, { + code: 'fr', + label: 'Français', + active: true, + }, { + code: 'lv', + label: 'Latviešu', + active: true, }], // Browse-By Pages browseBy: {