From 14b1fd463dd6930586d0fc5f5dad321afe0c8f49 Mon Sep 17 00:00:00 2001 From: Yura Date: Wed, 20 Oct 2021 11:27:55 +0200 Subject: [PATCH 001/435] 84367: SearchForm/SearchComponent: add option to hide scope selection dropdown --- .../search-page/configuration-search-page.component.ts | 6 ++++++ src/app/search-page/search.component.html | 1 + src/app/search-page/search.component.ts | 5 +++++ src/app/shared/search-form/search-form.component.html | 4 ++-- .../shared/search-form/search-form.component.spec.ts | 10 ++++++++++ src/app/shared/search-form/search-form.component.ts | 5 +++++ 6 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/app/search-page/configuration-search-page.component.ts b/src/app/search-page/configuration-search-page.component.ts index 1eefeeb569..5ba318b850 100644 --- a/src/app/search-page/configuration-search-page.component.ts +++ b/src/app/search-page/configuration-search-page.component.ts @@ -47,6 +47,12 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements */ @Input() fixedFilterQuery: string; + + /** + * Whether to show the scope selection dropdown + */ + @Input() scopeSelectable = true; + constructor(protected service: SearchService, protected sidebarService: SidebarService, protected windowService: HostWindowService, diff --git a/src/app/search-page/search.component.html b/src/app/search-page/search.component.html index d8aa25e4a3..499f652200 100644 --- a/src/app/search-page/search.component.html +++ b/src/app/search-page/search.component.html @@ -45,6 +45,7 @@ -
+
-
+
diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index 1469eac566..bbaee5709d 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -72,6 +72,16 @@ describe('SearchFormComponent', () => { expect(select).toBeNull(); }); + it('should not display scopes when scopeSelectable is false', () => { + comp.scopeSelectable = false; + comp.scopes = objects; + comp.scope = objects[1].id; + + fixture.detectChanges(); + const select = de.query(By.css('select')); + expect(select).toBeNull(); + }); + it('should display set query value in input field', fakeAsync(() => { const testString = 'This is a test query'; comp.query = testString; diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index 2791aee378..1cf3c11913 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -66,6 +66,11 @@ export class SearchFormComponent { */ @Output() submitSearch = new EventEmitter(); + /** + * Whether to show the scope selection dropdown + */ + @Input() scopeSelectable = true; + constructor(private router: Router, private searchService: SearchService, private paginationService: PaginationService, private searchConfig: SearchConfigurationService From 931560ee2627c2b7d130ea22006251e53adf1224 Mon Sep 17 00:00:00 2001 From: Yura Date: Thu, 21 Oct 2021 10:22:23 +0200 Subject: [PATCH 002/435] 84367: Make ComcolPageBrowseByComponent themeable --- .../browse-by-metadata-page.component.html | 2 +- .../collection-page.component.html | 4 +-- .../community-page.component.html | 4 +-- .../themed-comcol-page-browse-by.component.ts | 33 +++++++++++++++++++ src/app/shared/shared.module.ts | 2 ++ 5 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 src/app/shared/comcol-page-browse-by/themed-comcol-page-browse-by.component.ts diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html index 2321da0204..324a581ae0 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html @@ -18,7 +18,7 @@ - +
diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html index 9d598a3b69..6eceb696bd 100644 --- a/src/app/collection-page/collection-page.component.html +++ b/src/app/collection-page/collection-page.component.html @@ -40,10 +40,10 @@
- - +
diff --git a/src/app/community-page/community-page.component.html b/src/app/community-page/community-page.component.html index cf7282eb4b..27420e95b3 100644 --- a/src/app/community-page/community-page.component.html +++ b/src/app/community-page/community-page.component.html @@ -26,8 +26,8 @@
- - + + diff --git a/src/app/shared/comcol-page-browse-by/themed-comcol-page-browse-by.component.ts b/src/app/shared/comcol-page-browse-by/themed-comcol-page-browse-by.component.ts new file mode 100644 index 0000000000..410c1f53da --- /dev/null +++ b/src/app/shared/comcol-page-browse-by/themed-comcol-page-browse-by.component.ts @@ -0,0 +1,33 @@ +import { Component, Input } from '@angular/core'; +import { ThemedComponent } from '../theme-support/themed.component'; +import { ComcolPageBrowseByComponent } from './comcol-page-browse-by.component'; + +/** + * Themed wrapper for ComcolPageBrowseByComponent + */ +@Component({ + selector: 'ds-themed-comcol-page-browse-by', + styleUrls: [], + templateUrl: '../theme-support/themed.component.html', +}) +export class ThemedComcolPageBrowseByComponent extends ThemedComponent { + /** + * The ID of the Community or Collection + */ + @Input() id: string; + @Input() contentType: string; + + inAndOutputNames: (keyof ComcolPageBrowseByComponent & keyof this)[] = ['id', 'contentType']; + + protected getComponentName(): string { + return 'ComcolPageBrowseByComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/shared/comcol-page-browse-by/comcol-page-browse-by.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./comcol-page-browse-by.component'); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 9b993e551f..9d5a0151c4 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -233,6 +233,7 @@ import { OnClickMenuItemComponent } from './menu/menu-item/onclick-menu-item.com import { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component'; import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component'; import { SearchNavbarComponent } from '../search-navbar/search-navbar.component'; +import { ThemedComcolPageBrowseByComponent } from './comcol-page-browse-by/themed-comcol-page-browse-by.component'; /** * Declaration needed to make sure all decorator functions are called in time @@ -303,6 +304,7 @@ const COMPONENTS = [ EditComColPageComponent, DeleteComColPageComponent, ComcolPageBrowseByComponent, + ThemedComcolPageBrowseByComponent, ComcolRoleComponent, DsDynamicFormComponent, DsDynamicFormControlContainerComponent, From 859ff4a2f5e06c3c73fee0d8654fe4f391e76a4c Mon Sep 17 00:00:00 2001 From: Yura Date: Thu, 21 Oct 2021 10:33:17 +0200 Subject: [PATCH 003/435] 84367: Use DSONameService for ds-dso-selector names --- .../authorized-collection-selector.component.ts | 14 +++++++++----- .../dso-selector/dso-selector.component.html | 2 +- .../dso-selector/dso-selector.component.ts | 14 +++++++++++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts index bca1727542..08853307b0 100644 --- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts @@ -14,6 +14,7 @@ import { RemoteData } from '../../../../core/data/remote-data'; import { hasValue } from '../../../empty.util'; import { NotificationsService } from '../../../notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-authorized-collection-selector', @@ -24,11 +25,14 @@ import { TranslateService } from '@ngx-translate/core'; * Component rendering a list of collections to select from */ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent { - constructor(protected searchService: SearchService, - protected collectionDataService: CollectionDataService, - protected notifcationsService: NotificationsService, - protected translate: TranslateService) { - super(searchService, notifcationsService, translate); + constructor( + protected searchService: SearchService, + protected collectionDataService: CollectionDataService, + protected notifcationsService: NotificationsService, + protected translate: TranslateService, + protected dsoNameService: DSONameService, + ) { + super(searchService, notifcationsService, translate, dsoNameService); } /** diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index ab2ea6cd8b..a3b460fe90 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -22,7 +22,7 @@ -
- - - - -
-
-
-
-
    -
  • - -
  • -
-
- - -
- @@ -40,3 +17,4 @@ {{'browse.empty' | translate}} + diff --git a/src/app/shared/browse-by/browse-by.component.spec.ts b/src/app/shared/browse-by/browse-by.component.spec.ts index 896537b63a..a39a207c77 100644 --- a/src/app/shared/browse-by/browse-by.component.spec.ts +++ b/src/app/shared/browse-by/browse-by.component.spec.ts @@ -129,9 +129,9 @@ describe('BrowseByComponent', () => { expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).toBeDefined(); }); - describe('when enableArrows is true and objects are defined', () => { + describe('when showPaginator is true and objects are defined', () => { beforeEach(() => { - comp.enableArrows = true; + comp.showPaginator = true; comp.objects$ = mockItemsRD$; comp.paginationConfig = paginationConfig; @@ -188,7 +188,7 @@ describe('BrowseByComponent', () => { }); }); - describe('when enableArrows is true and browseEntries are provided', () => { + describe('when showPaginator is true and browseEntries are provided', () => { let browseEntries; beforeEach(() => { @@ -209,7 +209,7 @@ describe('BrowseByComponent', () => { }), ]; - comp.enableArrows = true; + comp.showPaginator = true; comp.objects$ = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), browseEntries)); comp.paginationConfig = paginationConfig; comp.sortConfig = Object.assign(new SortOptions('dc.title', SortDirection.ASC)); diff --git a/src/app/shared/browse-by/browse-by.component.ts b/src/app/shared/browse-by/browse-by.component.ts index 861e431635..8245897609 100644 --- a/src/app/shared/browse-by/browse-by.component.ts +++ b/src/app/shared/browse-by/browse-by.component.ts @@ -67,30 +67,30 @@ export class BrowseByComponent implements OnInit { /** * Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination */ - @Input() enableArrows = false; + @Input() showPaginator = true; /** - * If enableArrows is set to true, should it hide the options gear? + * If showPaginator is set to true, should it hide the options gear? */ @Input() hideGear = false; /** - * If enableArrows is set to true, emit when the previous button is clicked + * If showPaginator is set to true, emit when the previous button is clicked */ @Output() prev = new EventEmitter(); /** - * If enableArrows is set to true, emit when the next button is clicked + * If showPaginator is set to true, emit when the next button is clicked */ @Output() next = new EventEmitter(); /** - * If enableArrows is set to true, emit when the page size is changed + * If showPaginator is set to true, emit when the page size is changed */ @Output() pageSizeChange = new EventEmitter(); /** - * If enableArrows is set to true, emit when the sort direction is changed + * If showPaginator is set to true, emit when the sort direction is changed */ @Output() sortDirectionChange = new EventEmitter(); diff --git a/src/app/shared/object-collection/object-collection.component.html b/src/app/shared/object-collection/object-collection.component.html index f2778757ef..999ae9a120 100644 --- a/src/app/shared/object-collection/object-collection.component.html +++ b/src/app/shared/object-collection/object-collection.component.html @@ -6,6 +6,7 @@ [linkType]="linkType" [context]="context" [hidePaginationDetail]="hidePaginationDetail" + [showPaginator]="showPaginator" (paginationChange)="onPaginationChange($event)" (pageChange)="onPageChange($event)" (pageSizeChange)="onPageSizeChange($event)" @@ -19,6 +20,8 @@ [importConfig]="importConfig" (importObject)="importObject.emit($event)" (contentChange)="contentChange.emit()" + (prev)="goPrev()" + (next)="goNext()" *ngIf="(currentMode$ | async) === viewModeEnum.ListElement"> @@ -29,6 +32,7 @@ [linkType]="linkType" [context]="context" [hidePaginationDetail]="hidePaginationDetail" + [showPaginator]="showPaginator" (paginationChange)="onPaginationChange($event)" (pageChange)="onPageChange($event)" (pageSizeChange)="onPageSizeChange($event)" @@ -44,6 +48,7 @@ [linkType]="linkType" [context]="context" [hidePaginationDetail]="hidePaginationDetail" + [showPaginator]="showPaginator" *ngIf="(currentMode$ | async) === viewModeEnum.DetailedListElement"> diff --git a/src/app/shared/object-collection/object-collection.component.ts b/src/app/shared/object-collection/object-collection.component.ts index 29fdb37ea1..f2706f1a4c 100644 --- a/src/app/shared/object-collection/object-collection.component.ts +++ b/src/app/shared/object-collection/object-collection.component.ts @@ -88,6 +88,11 @@ export class ObjectCollectionComponent implements OnInit { */ @Input() hidePaginationDetail = false; + /** + * Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination + */ + @Input() showPaginator = true; + /** * the page info of the list */ @@ -122,6 +127,16 @@ export class ObjectCollectionComponent implements OnInit { */ @Output() sortFieldChange: EventEmitter = new EventEmitter(); + /** + * If showPaginator is set to true, emit when the previous button is clicked + */ + @Output() prev = new EventEmitter(); + + /** + * If showPaginator is set to true, emit when the next button is clicked + */ + @Output() next = new EventEmitter(); + /** * Emits the current view mode */ @@ -192,4 +207,18 @@ export class ObjectCollectionComponent implements OnInit { this.paginationChange.emit(event); } + /** + * Go to the previous page + */ + goPrev() { + this.prev.emit(true); + } + + /** + * Go to the next page + */ + goNext() { + this.next.emit(true); + } + } diff --git a/src/app/shared/object-detail/object-detail.component.html b/src/app/shared/object-detail/object-detail.component.html index 7fef7d9689..32728d92e4 100644 --- a/src/app/shared/object-detail/object-detail.component.html +++ b/src/app/shared/object-detail/object-detail.component.html @@ -3,14 +3,19 @@ [pageInfoState]="objects?.payload" [collectionSize]="objects?.payload?.totalElements" [sortOptions]="sortConfig" + [objects]="objects" [hideGear]="hideGear" [hidePaginationDetail]="hidePaginationDetail" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage" + [showPaginator]="showPaginator" (pageChange)="onPageChange($event)" (pageSizeChange)="onPageSizeChange($event)" (sortDirectionChange)="onSortDirectionChange($event)" (sortFieldChange)="onSortFieldChange($event)" - (paginationChange)="onPaginationChange($event)"> + (paginationChange)="onPaginationChange($event)" + (prev)="goPrev()" + (next)="goNext()" + >
diff --git a/src/app/shared/object-detail/object-detail.component.ts b/src/app/shared/object-detail/object-detail.component.ts index 8b94746fbd..05d2ca4b5b 100644 --- a/src/app/shared/object-detail/object-detail.component.ts +++ b/src/app/shared/object-detail/object-detail.component.ts @@ -66,6 +66,21 @@ export class ObjectDetailComponent { */ @Input() context: Context; + /** + * Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination + */ + @Input() showPaginator = true; + + /** + * If showPaginator is set to true, emit when the previous button is clicked + */ + @Output() prev = new EventEmitter(); + + /** + * If showPaginator is set to true, emit when the next button is clicked + */ + @Output() next = new EventEmitter(); + /** * The list of objects to paginate */ @@ -168,4 +183,18 @@ export class ObjectDetailComponent { this.paginationChange.emit(event); } + /** + * Go to the previous page + */ + goPrev() { + this.prev.emit(true); + } + + /** + * Go to the next page + */ + goNext() { + this.next.emit(true); + } + } diff --git a/src/app/shared/object-grid/object-grid.component.html b/src/app/shared/object-grid/object-grid.component.html index 0afd623d86..0961aa2989 100644 --- a/src/app/shared/object-grid/object-grid.component.html +++ b/src/app/shared/object-grid/object-grid.component.html @@ -4,13 +4,18 @@ [collectionSize]="objects?.payload?.totalElements" [sortOptions]="sortConfig" [hideGear]="hideGear" + [objects]="objects" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage" [hidePaginationDetail]="hidePaginationDetail" + [showPaginator]="showPaginator" (pageChange)="onPageChange($event)" (pageSizeChange)="onPageSizeChange($event)" (sortDirectionChange)="onSortDirectionChange($event)" (sortFieldChange)="onSortFieldChange($event)" - (paginationChange)="onPaginationChange($event)"> + (paginationChange)="onPaginationChange($event)" + (prev)="goPrev()" + (next)="goNext()" + >
@@ -22,3 +27,4 @@ + diff --git a/src/app/shared/object-grid/object-grid.component.ts b/src/app/shared/object-grid/object-grid.component.ts index 39c3d56f7f..1b5ab075e2 100644 --- a/src/app/shared/object-grid/object-grid.component.ts +++ b/src/app/shared/object-grid/object-grid.component.ts @@ -49,6 +49,11 @@ export class ObjectGridComponent implements OnInit { */ @Input() sortConfig: SortOptions; + /** + * Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination + */ + @Input() showPaginator = true; + /** * The whether or not the gear is hidden */ @@ -134,6 +139,17 @@ export class ObjectGridComponent implements OnInit { * Event's payload equals to the newly selected sort field. */ @Output() sortFieldChange: EventEmitter = new EventEmitter(); + + /** + * If showPaginator is set to true, emit when the previous button is clicked + */ + @Output() prev = new EventEmitter(); + + /** + * If showPaginator is set to true, emit when the next button is clicked + */ + @Output() next = new EventEmitter(); + data: any = {}; columns$: Observable; @@ -225,4 +241,18 @@ export class ObjectGridComponent implements OnInit { this.paginationChange.emit(event); } + /** + * Go to the previous page + */ + goPrev() { + this.prev.emit(true); + } + + /** + * Go to the next page + */ + goNext() { + this.next.emit(true); + } + } diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index 331ff1cb28..1c6d01ba11 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -2,15 +2,20 @@ [paginationOptions]="config" [pageInfoState]="objects?.payload" [collectionSize]="objects?.payload?.totalElements" + [objects]="objects" [sortOptions]="sortConfig" [hideGear]="hideGear" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage" [hidePaginationDetail]="hidePaginationDetail" + [showPaginator]="showPaginator" (pageChange)="onPageChange($event)" (pageSizeChange)="onPageSizeChange($event)" (sortDirectionChange)="onSortDirectionChange($event)" (sortFieldChange)="onSortFieldChange($event)" - (paginationChange)="onPaginationChange($event)"> + (paginationChange)="onPaginationChange($event)" + (prev)="goPrev()" + (next)="goNext()" + >
  • (); + /** + * If showPaginator is set to true, emit when the previous button is clicked + */ + @Output() prev = new EventEmitter(); + + /** + * If showPaginator is set to true, emit when the next button is clicked + */ + @Output() next = new EventEmitter(); + /** * The current listable objects */ @@ -192,4 +207,18 @@ export class ObjectListComponent { onPaginationChange(event) { this.paginationChange.emit(event); } + + /** + * Go to the previous page + */ + goPrev() { + this.prev.emit(true); + } + + /** + * Go to the next page + */ + goNext() { + this.next.emit(true); + } } diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index 2a9aa1a062..785d38af96 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -1,7 +1,7 @@
    -
    +
    {{ 'pagination.showing.label' | translate }} {{ 'pagination.showing.detail' | translate:(getShowingDetails(collectionSize)|async)}}
    @@ -20,16 +20,31 @@
    - diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 8f1c6bf26f..21f495956d 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -20,6 +20,10 @@ import { hasValue } from '../empty.util'; import { PageInfo } from '../../core/shared/page-info.model'; import { PaginationService } from '../../core/pagination/pagination.service'; import { map } from 'rxjs/operators'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { PaginatedList } from 'src/app/core/data/paginated-list.model'; +import { ListableObject } from '../object-collection/shared/listable-object.model'; +import { ViewMode } from 'src/app/core/shared/view-mode.model'; /** * The default pagination controls component. @@ -33,6 +37,11 @@ import { map } from 'rxjs/operators'; encapsulation: ViewEncapsulation.Emulated }) export class PaginationComponent implements OnDestroy, OnInit { + /** + * ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}. + */ + viewMode: ViewMode = ViewMode.ListElement; + /** * Number of items in collection. */ @@ -53,6 +62,26 @@ export class PaginationComponent implements OnDestroy, OnInit { */ @Input() sortOptions: SortOptions; + /** + * Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination + */ + @Input() showPaginator = true; + + /** + * The current pagination configuration + */ + @Input() config?: PaginationComponentOptions; + + /** + * The list of listable objects to render in this component + */ + @Input() objects: RemoteData>; + + /** + * The current sorting configuration + */ + @Input() sortConfig: SortOptions; + /** * An event fired when the page is changed. * Event's payload equals to the newly selected page. @@ -163,6 +192,15 @@ export class PaginationComponent implements OnDestroy, OnInit { */ private subs: Subscription[] = []; + /** + * If showPaginator is set to true, emit when the previous button is clicked + */ + @Output() prev = new EventEmitter(); + + /** + * If showPaginator is set to true, emit when the next button is clicked + */ + @Output() next = new EventEmitter(); /** * Method provided by Angular. Invoked after the constructor. */ @@ -347,4 +385,19 @@ export class PaginationComponent implements OnDestroy, OnInit { map((hasMultiplePages) => hasMultiplePages || !this.hidePagerWhenSinglePage) ); } + + /** + * Go to the previous page + */ + goPrev() { + this.prev.emit(true); + } + + /** + * Go to the next page + */ + goNext() { + this.next.emit(true); + } + } From 57ff37ec7f1b1caeb9860b9b3a87489b5402d542 Mon Sep 17 00:00:00 2001 From: Pratik Rajkotiya Date: Wed, 2 Feb 2022 19:11:06 +0530 Subject: [PATCH 015/435] [DSC-389] merged with main branch --- config/config.example.yml | 3 + src/app/app-routing-paths.ts | 6 + .../edit-bitstream-page.component.html | 2 +- .../edit-bitstream-page.component.scss | 4 + .../edit-bitstream-page.component.spec.ts | 539 +- .../edit-bitstream-page.component.ts | 264 +- src/app/core/auth/models/auth.method-type.ts | 3 +- src/app/core/auth/models/auth.method.ts | 5 + ...onfig-accesses-conditions-options.model.ts | 45 + .../models/config-submission-access.model.ts | 42 + .../config-submission-accesses.model.ts | 10 + src/app/core/config/models/config-type.ts | 2 + .../submission-accesses-config.service.ts | 42 + src/app/core/core.module.ts | 14 +- .../data/feature-authorization/feature-id.ts | 1 + .../feedback/feedback-data.service.spec.ts | 88 + .../core/feedback/feedback-data.service.ts | 49 + src/app/core/feedback/feedback.guard.ts | 20 + .../core/feedback/models/feedback.model.ts | 34 + .../feedback/models/feedback.resource-type.ts | 9 + .../models/access-condition.model.ts | 25 + .../submission-accesses.resource-type.ts | 9 + .../submission-item-access-condition.model.ts | 8 + ...sion-upload-file-access-condition.model.ts | 23 +- .../workspaceitem-section-accesses.model.ts | 21 + .../models/workspaceitem-sections.model.ts | 2 + src/app/footer/footer.component.html | 4 + .../feedback-form.component.html | 45 + .../feedback-form.component.scss | 3 + .../feedback-form.component.spec.ts | 97 + .../feedback-form/feedback-form.component.ts | 83 + src/app/info/feedback/feedback.component.html | 3 + src/app/info/feedback/feedback.component.scss | 0 .../info/feedback/feedback.component.spec.ts | 27 + src/app/info/feedback/feedback.component.ts | 12 + .../feedback/themed-feedback.component.ts | 26 + src/app/info/info-routing-paths.ts | 5 + src/app/info/info-routing.module.ts | 14 +- src/app/info/info.module.ts | 15 +- .../item-delete/item-delete.component.spec.ts | 72 +- .../item-delete/item-delete.component.ts | 59 +- ...amic-form-control-container.component.html | 3 +- .../dynamic-form-array.component.html | 4 +- .../dynamic-form-array.component.scss | 10 +- .../methods/oidc/log-in-oidc.component.html | 3 + .../oidc/log-in-oidc.component.spec.ts | 155 + .../methods/oidc/log-in-oidc.component.ts | 110 + .../shared/mocks/mock-native-window-ref.ts | 3 +- .../section-accesses-config.service.mock.ts | 87 + .../mocks/section-accesses.service.mock.ts | 13 + src/app/shared/mocks/submission.mock.ts | 93 +- src/app/shared/shared.module.ts | 3 + src/app/shared/testing/form-event.stub.ts | 100 + src/app/shared/testing/route-service.stub.ts | 5 +- .../form/submission-form.component.ts | 2 +- .../objects/submission-objects.effects.ts | 6 +- .../accesses/section-accesses.component.html | 7 + .../accesses/section-accesses.component.scss | 5 + .../section-accesses.component.spec.ts | 204 + .../accesses/section-accesses.component.ts | 379 + .../accesses/section-accesses.model.ts | 123 + .../accesses/section-accesses.service.ts | 42 + .../container/section-container.component.ts | 6 +- .../form/section-form.component.spec.ts | 16 +- .../sections/form/section-form.component.ts | 2 +- .../sections/models/section.model.ts | 4 +- src/app/submission/sections/sections-type.ts | 3 +- .../edit/section-upload-file-edit.model.ts | 5 +- .../file/section-upload-file.component.ts | 12 +- src/app/submission/selectors.ts | 11 +- src/app/submission/submission.module.ts | 58 +- src/assets/i18n/en.json5 | 84 + src/assets/i18n/gd.json5 | 6076 +++++++++++++++++ src/config/default-app-config.ts | 1 + .../app/info/feedback/feedback.component.html | 0 .../app/info/feedback/feedback.component.scss | 0 .../app/info/feedback/feedback.component.ts | 15 + src/themes/custom/theme.module.ts | 4 +- 78 files changed, 9098 insertions(+), 291 deletions(-) create mode 100644 src/app/core/config/models/config-accesses-conditions-options.model.ts create mode 100644 src/app/core/config/models/config-submission-access.model.ts create mode 100644 src/app/core/config/models/config-submission-accesses.model.ts create mode 100644 src/app/core/config/submission-accesses-config.service.ts create mode 100644 src/app/core/feedback/feedback-data.service.spec.ts create mode 100644 src/app/core/feedback/feedback-data.service.ts create mode 100644 src/app/core/feedback/feedback.guard.ts create mode 100644 src/app/core/feedback/models/feedback.model.ts create mode 100644 src/app/core/feedback/models/feedback.resource-type.ts create mode 100644 src/app/core/submission/models/access-condition.model.ts create mode 100644 src/app/core/submission/models/submission-accesses.resource-type.ts create mode 100644 src/app/core/submission/models/submission-item-access-condition.model.ts create mode 100644 src/app/core/submission/models/workspaceitem-section-accesses.model.ts create mode 100644 src/app/info/feedback/feedback-form/feedback-form.component.html create mode 100644 src/app/info/feedback/feedback-form/feedback-form.component.scss create mode 100644 src/app/info/feedback/feedback-form/feedback-form.component.spec.ts create mode 100644 src/app/info/feedback/feedback-form/feedback-form.component.ts create mode 100644 src/app/info/feedback/feedback.component.html create mode 100644 src/app/info/feedback/feedback.component.scss create mode 100644 src/app/info/feedback/feedback.component.spec.ts create mode 100644 src/app/info/feedback/feedback.component.ts create mode 100644 src/app/info/feedback/themed-feedback.component.ts create mode 100644 src/app/shared/log-in/methods/oidc/log-in-oidc.component.html create mode 100644 src/app/shared/log-in/methods/oidc/log-in-oidc.component.spec.ts create mode 100644 src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts create mode 100644 src/app/shared/mocks/section-accesses-config.service.mock.ts create mode 100644 src/app/shared/mocks/section-accesses.service.mock.ts create mode 100644 src/app/shared/testing/form-event.stub.ts create mode 100644 src/app/submission/sections/accesses/section-accesses.component.html create mode 100644 src/app/submission/sections/accesses/section-accesses.component.scss create mode 100644 src/app/submission/sections/accesses/section-accesses.component.spec.ts create mode 100644 src/app/submission/sections/accesses/section-accesses.component.ts create mode 100644 src/app/submission/sections/accesses/section-accesses.model.ts create mode 100644 src/app/submission/sections/accesses/section-accesses.service.ts create mode 100644 src/assets/i18n/gd.json5 create mode 100644 src/themes/custom/app/info/feedback/feedback.component.html create mode 100644 src/themes/custom/app/info/feedback/feedback.component.scss create mode 100644 src/themes/custom/app/info/feedback/feedback.component.ts diff --git a/config/config.example.yml b/config/config.example.yml index 1e035889a5..ecb2a3cfb9 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -127,6 +127,9 @@ languages: - code: fr label: Français active: true + - code: gd + label: Gàidhlig + active: true - code: lv label: Latviešu active: true diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index d9c3410931..57767b6f3e 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -32,6 +32,12 @@ export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: st }; } +export const HOME_PAGE_PATH = 'admin'; + +export function getHomePageRoute() { + return `/${HOME_PAGE_PATH}`; +} + export const ADMIN_MODULE_PATH = 'admin'; export function getAdminModuleRoute() { 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 index cbb587cca4..4d3b948a58 100644 --- 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 @@ -12,7 +12,7 @@
    - div > label { + margin-top: 1.75rem; +} + 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 index 67f8866e6d..44e48182fd 100644 --- 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 @@ -22,6 +22,8 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } f import { getEntityEditRoute } from '../../item-page/item-page-routing-paths'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { Item } from '../../core/shared/item.model'; +import { MetadataValueFilter } from '../../core/shared/metadata.models'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -31,24 +33,27 @@ let notificationsService: NotificationsService; let formService: DynamicFormService; let bitstreamService: BitstreamDataService; let bitstreamFormatService: BitstreamFormatDataService; +let dsoNameService: DSONameService; let bitstream: Bitstream; let selectedFormat: BitstreamFormat; let allFormats: BitstreamFormat[]; let router: Router; -describe('EditBitstreamPageComponent', () => { - let comp: EditBitstreamPageComponent; - let fixture: ComponentFixture; +let comp: EditBitstreamPageComponent; +let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { +describe('EditBitstreamPageComponent', () => { + + beforeEach(() => { allFormats = [ Object.assign({ id: '1', shortDescription: 'Unknown', description: 'Unknown format', supportLevel: BitstreamFormatSupportLevel.Unknown, + mimetype: 'application/octet-stream', _links: { - self: { href: 'format-selflink-1' } + self: {href: 'format-selflink-1'} } }), Object.assign({ @@ -56,8 +61,9 @@ describe('EditBitstreamPageComponent', () => { shortDescription: 'PNG', description: 'Portable Network Graphics', supportLevel: BitstreamFormatSupportLevel.Known, + mimetype: 'image/png', _links: { - self: { href: 'format-selflink-2' } + self: {href: 'format-selflink-2'} } }), Object.assign({ @@ -65,19 +71,14 @@ describe('EditBitstreamPageComponent', () => { shortDescription: 'GIF', description: 'Graphics Interchange Format', supportLevel: BitstreamFormatSupportLevel.Known, + mimetype: 'image/gif', _links: { - self: { href: 'format-selflink-3' } + 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 = {}; @@ -90,156 +91,418 @@ describe('EditBitstreamPageComponent', () => { return undefined; } }); - bitstream = Object.assign(new Bitstream(), { - metadata: { - 'dc.description': [ - { - value: 'Bitstream description' - } - ], - 'dc.title': [ - { - value: 'Bitstream title' - } - ] - }, - format: createSuccessfulRemoteDataObject$(selectedFormat), - _links: { - self: 'bitstream-selflink' - }, - bundle: createSuccessfulRemoteDataObject$({ - item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { - uuid: 'some-uuid' - })) - }) - }); - bitstreamService = jasmine.createSpyObj('bitstreamService', { - findById: createSuccessfulRemoteDataObject$(bitstream), - update: createSuccessfulRemoteDataObject$(bitstream), - updateFormat: createSuccessfulRemoteDataObject$(bitstream), - commitUpdates: {}, - patch: {} - }); + bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: createSuccessfulRemoteDataObject$(createPaginatedList(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: createSuccessfulRemoteDataObject(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(); - router = TestBed.inject(Router); - spyOn(router, 'navigate'); + notificationsService = jasmine.createSpyObj('notificationsService', + { + info: infoNotification, + warning: warningNotification, + success: successNotification + } + ); }); - describe('on startup', () => { - let rawForm; + describe('EditBitstreamPageComponent no IIIF fields', () => { + + beforeEach(waitForAsync(() => { + + const bundleName = 'ORIGINAL'; + + bitstream = Object.assign(new Bitstream(), { + metadata: { + 'dc.description': [ + { + value: 'Bitstream description' + } + ], + 'dc.title': [ + { + value: 'Bitstream title' + } + ] + }, + format: createSuccessfulRemoteDataObject$(selectedFormat), + _links: { + self: 'bitstream-selflink' + }, + bundle: createSuccessfulRemoteDataObject$({ + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return undefined; + }, + })) + }) + }); + bitstreamService = jasmine.createSpyObj('bitstreamService', { + findById: createSuccessfulRemoteDataObject$(bitstream), + update: createSuccessfulRemoteDataObject$(bitstream), + updateFormat: createSuccessfulRemoteDataObject$(bitstream), + commitUpdates: {}, + patch: {} + }); + bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats)) + }); + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: bundleName + }); + + 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: createSuccessfulRemoteDataObject(bitstream)}), + snapshot: {queryParams: {}} + } + }, + {provide: BitstreamDataService, useValue: bitstreamService}, + {provide: DSONameService, useValue: dsoNameService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + ChangeDetectorRef + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + })); beforeEach(() => { - rawForm = comp.formGroup.getRawValue(); + fixture = TestBed.createComponent(EditBitstreamPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + router = TestBed.inject(Router); + spyOn(router, 'navigate'); }); - it('should fill in the bitstream\'s title', () => { - expect(rawForm.fileNamePrimaryContainer.fileName).toEqual(bitstream.name); - }); + describe('on startup', () => { + let rawForm; - 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(); + rawForm = comp.formGroup.getRawValue(); }); - it('should call update', () => { - expect(bitstreamService.update).toHaveBeenCalled(); + it('should fill in the bitstream\'s title', () => { + expect(rawForm.fileNamePrimaryContainer.fileName).toEqual(bitstream.name); }); - it('should commit the updates', () => { - expect(bitstreamService.commitUpdates).toHaveBeenCalled(); + 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 selected format has changed', () => { + describe('when an unknown format is selected', () => { beforeEach(() => { - comp.formGroup.patchValue({ - formatContainer: { - selectedFormat: allFormats[2].id - } + 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(); + }); + }); + }); + describe('when the cancel button is clicked', () => { + it('should call navigateToItemEditBitstreams method', () => { + spyOn(comp, 'navigateToItemEditBitstreams'); + comp.onCancel(); + expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled(); + }); + }); + describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => { + it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => { + comp.itemId = 'some-uuid1'; + comp.navigateToItemEditBitstreams(); + expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']); + }); + }); + describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => { + it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => { + comp.itemId = undefined; + comp.navigateToItemEditBitstreams(); + expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']); + }); + }); + }); + + describe('EditBitstreamPageComponent with IIIF fields', () => { + + const bundleName = 'ORIGINAL'; + + beforeEach(waitForAsync(() => { + + bitstream = Object.assign(new Bitstream(), { + metadata: { + 'dc.description': [ + { + value: 'Bitstream description' + } + ], + 'dc.title': [ + { + value: 'Bitstream title' + } + ], + 'iiif.label': [ + { + value: 'chapter one' + } + ], + 'iiif.toc': [ + { + value: 'chapter one' + } + ], + 'iiif.image.width': [ + { + value: '2400' + } + ], + 'iiif.image.height': [ + { + value: '2800' + } + ], + }, + format: createSuccessfulRemoteDataObject$(allFormats[1]), + _links: { + self: 'bitstream-selflink' + }, + bundle: createSuccessfulRemoteDataObject$({ + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return 'True'; + } + })) + }) + }); + bitstreamService = jasmine.createSpyObj('bitstreamService', { + findById: createSuccessfulRemoteDataObject$(bitstream), + update: createSuccessfulRemoteDataObject$(bitstream), + updateFormat: createSuccessfulRemoteDataObject$(bitstream), + commitUpdates: {}, + patch: {} + }); + + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: bundleName + }); + + 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: createSuccessfulRemoteDataObject(bitstream)}), + snapshot: {queryParams: {}} + } + }, + {provide: BitstreamDataService, useValue: bitstreamService}, + {provide: DSONameService, useValue: dsoNameService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + ChangeDetectorRef + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditBitstreamPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + router = TestBed.inject(Router); + spyOn(router, 'navigate'); + }); + + + describe('on startup', () => { + let rawForm; + + beforeEach(() => { + rawForm = comp.formGroup.getRawValue(); + }); + it('should set isIIIF to true', () => { + expect(comp.isIIIF).toBeTrue(); + }); + it('should fill in the iiif label', () => { + expect(rawForm.iiifLabelContainer.iiifLabel).toEqual('chapter one'); + }); + it('should fill in the iiif toc', () => { + expect(rawForm.iiifTocContainer.iiifToc).toEqual('chapter one'); + }); + it('should fill in the iiif width', () => { + expect(rawForm.iiifWidthContainer.iiifWidth).toEqual('2400'); + }); + it('should fill in the iiif height', () => { + expect(rawForm.iiifHeightContainer.iiifHeight).toEqual('2800'); + }); + }); + }); + + describe('ignore OTHERCONTENT bundle', () => { + + const bundleName = 'OTHERCONTENT'; + + beforeEach(waitForAsync(() => { + + bitstream = Object.assign(new Bitstream(), { + metadata: { + 'dc.description': [ + { + value: 'Bitstream description' + } + ], + 'dc.title': [ + { + value: 'Bitstream title' + } + ], + 'iiif.label': [ + { + value: 'chapter one' + } + ], + 'iiif.toc': [ + { + value: 'chapter one' + } + ], + 'iiif.image.width': [ + { + value: '2400' + } + ], + 'iiif.image.height': [ + { + value: '2800' + } + ], + }, + format: createSuccessfulRemoteDataObject$(allFormats[2]), + _links: { + self: 'bitstream-selflink' + }, + bundle: createSuccessfulRemoteDataObject$({ + item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), { + uuid: 'some-uuid', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return 'True'; + } + })) + }) + }); + bitstreamService = jasmine.createSpyObj('bitstreamService', { + findById: createSuccessfulRemoteDataObject$(bitstream), + update: createSuccessfulRemoteDataObject$(bitstream), + updateFormat: createSuccessfulRemoteDataObject$(bitstream), + commitUpdates: {}, + patch: {} + }); + + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: bundleName + }); + + 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: createSuccessfulRemoteDataObject(bitstream)}), + snapshot: {queryParams: {}} + } + }, + {provide: BitstreamDataService, useValue: bitstreamService}, + {provide: DSONameService, useValue: dsoNameService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatService}, + ChangeDetectorRef + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditBitstreamPageComponent); + comp = fixture.componentInstance; fixture.detectChanges(); - comp.onSubmit(); + router = TestBed.inject(Router); + spyOn(router, 'navigate'); }); - it('should call update', () => { - expect(bitstreamService.update).toHaveBeenCalled(); - }); + describe('EditBitstreamPageComponent with IIIF fields', () => { + let rawForm; - it('should call updateFormat', () => { - expect(bitstreamService.updateFormat).toHaveBeenCalled(); - }); + beforeEach(() => { + rawForm = comp.formGroup.getRawValue(); + }); - it('should commit the updates', () => { - expect(bitstreamService.commitUpdates).toHaveBeenCalled(); + it('should NOT set isIIIF to true', () => { + expect(comp.isIIIF).toBeFalse(); + }); + it('should put the \"IIIF Label\" input not to be shown', () => { + expect(rawForm.iiifLabelContainer).toBeFalsy(); + }); }); - }); - }); - describe('when the cancel button is clicked', () => { - it('should call navigateToItemEditBitstreams method', () => { - spyOn(comp, 'navigateToItemEditBitstreams'); - comp.onCancel(); - expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled(); - }); - }); - describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => { - it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => { - comp.itemId = 'some-uuid1'; - comp.navigateToItemEditBitstreams(); - expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']); - }); - }); - describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => { - it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => { - comp.itemId = undefined; - comp.navigateToItemEditBitstreams(); - expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']); - }); }); + }); 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 index f6ece7f4fa..9a59df4b95 100644 --- 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 @@ -1,16 +1,27 @@ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit +} from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; import { ActivatedRoute, Router } from '@angular/router'; import { map, mergeMap, switchMap } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { + combineLatest, + combineLatest as observableCombineLatest, + Observable, + of as observableOf, + Subscription +} from 'rxjs'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, - DynamicSelectModel, - DynamicTextAreaModel + DynamicSelectModel } from '@ng-dynamic-forms/core'; import { FormGroup } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; @@ -28,13 +39,19 @@ import { NotificationsService } from '../../shared/notifications/notifications.s 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 { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty, isEmpty } from '../../shared/empty.util'; import { Metadata } from '../../core/shared/metadata.utils'; import { Location } from '@angular/common'; import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { getEntityEditRoute, getItemEditRoute } from '../../item-page/item-page-routing-paths'; import { Bundle } from '../../core/shared/bundle.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { Item } from '../../core/shared/item.model'; +import { + DsDynamicInputModel +} from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; @Component({ selector: 'ds-edit-bitstream-page', @@ -94,6 +111,26 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ NOTIFICATIONS_PREFIX = 'bitstream.edit.notifications.'; + /** + * IIIF image width metadata key + */ + IMAGE_WIDTH_METADATA = 'iiif.image.width'; + + /** + * IIIF image height metadata key + */ + IMAGE_HEIGHT_METADATA = 'iiif.image.height'; + + /** + * IIIF table of contents metadata key + */ + IIIF_TOC_METADATA = 'iiif.toc'; + + /** + * IIIF label metadata key + */ + IIIF_LABEL_METADATA = 'iiif.label'; + /** * Options for fetching all bitstream formats */ @@ -102,7 +139,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { /** * The Dynamic Input Model for the file's name */ - fileNameModel = new DynamicInputModel({ + fileNameModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'fileName', name: 'fileName', required: true, @@ -118,14 +156,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * The Dynamic Switch Model for the file's name */ primaryBitstreamModel = new DynamicCustomSwitchModel({ - id: 'primaryBitstream', - name: 'primaryBitstream' - }); + id: 'primaryBitstream', + name: 'primaryBitstream' + } + ); /** * The Dynamic TextArea Model for the file's description */ - descriptionModel = new DynamicTextAreaModel({ + descriptionModel = new DsDynamicTextAreaModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', id: 'description', name: 'description', rows: 10 @@ -147,10 +187,87 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { name: 'newFormat' }); + /** + * The Dynamic Input Model for the iiif label + */ + iiifLabelModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifLabel', + name: 'iiifLabel' + }, + { + grid: { + host: 'col col-lg-6 d-inline-block' + } + }); + iiifLabelContainer = new DynamicFormGroupModel({ + id: 'iiifLabelContainer', + group: [this.iiifLabelModel] + },{ + grid: { + host: 'form-row' + } + }); + + iiifTocModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifToc', + name: 'iiifToc', + },{ + grid: { + host: 'col col-lg-6 d-inline-block' + } + }); + iiifTocContainer = new DynamicFormGroupModel({ + id: 'iiifTocContainer', + group: [this.iiifTocModel] + },{ + grid: { + host: 'form-row' + } + }); + + iiifWidthModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifWidth', + name: 'iiifWidth', + },{ + grid: { + host: 'col col-lg-6 d-inline-block' + } + }); + iiifWidthContainer = new DynamicFormGroupModel({ + id: 'iiifWidthContainer', + group: [this.iiifWidthModel] + },{ + grid: { + host: 'form-row' + } + }); + + iiifHeightModel = new DsDynamicInputModel({ + hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '', + id: 'iiifHeight', + name: 'iiifHeight' + },{ + grid: { + host: 'col col-lg-6 d-inline-block' + } + }); + iiifHeightContainer = new DynamicFormGroupModel({ + id: 'iiifHeightContainer', + group: [this.iiifHeightModel] + },{ + grid: { + host: 'form-row' + } + }); + /** * All input models in a simple array for easier iterations */ - inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, this.newFormatModel]; + inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, + this.newFormatModel]; /** * The dynamic form fields used for editing the information of a bitstream @@ -163,7 +280,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.fileNameModel, this.primaryBitstreamModel ] - }), + },{ + grid: { + host: 'form-row' + } + }), new DynamicFormGroupModel({ id: 'descriptionContainer', group: [ @@ -254,18 +375,27 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { */ entityType: string; + /** + * Set to true when the parent item supports IIIF. + */ + isIIIF = false; + + /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} */ protected subs: Subscription[] = []; + constructor(private route: ActivatedRoute, private router: Router, + private changeDetectorRef: ChangeDetectorRef, private location: Location, private formService: DynamicFormService, private translate: TranslateService, private bitstreamService: BitstreamDataService, + private dsoNameService: DSONameService, private notificationsService: NotificationsService, private bitstreamFormatService: BitstreamFormatDataService) { } @@ -277,7 +407,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { * - Translate the form labels and hints */ ngOnInit(): void { - this.formGroup = this.formService.createFormGroup(this.formModel); this.itemId = this.route.snapshot.queryParams.itemId; this.entityType = this.route.snapshot.queryParams.entityType; @@ -301,13 +430,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { ).subscribe(([bitstream, allFormats]) => { this.bitstream = bitstream as Bitstream; this.formats = allFormats.page; - this.updateFormatModel(); - this.updateForm(this.bitstream); + this.setIiifStatus(this.bitstream); }) ); - this.updateFieldTranslations(); - this.subs.push( this.translate.onLangChange .subscribe(() => { @@ -316,6 +442,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { ); } + /** + * Initializes the form. + */ + setForm() { + this.formGroup = this.formService.createFormGroup(this.formModel); + this.updateFormatModel(); + this.updateForm(this.bitstream); + this.updateFieldTranslations(); + } + /** * Update the current form values with bitstream properties * @param bitstream @@ -333,6 +469,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined } }); + if (this.isIIIF) { + this.formGroup.patchValue({ + iiifLabelContainer: { + iiifLabel: bitstream.firstMetadataValue(this.IIIF_LABEL_METADATA) + }, + iiifTocContainer: { + iiifToc: bitstream.firstMetadataValue(this.IIIF_TOC_METADATA) + }, + iiifWidthContainer: { + iiifWidth: bitstream.firstMetadataValue(this.IMAGE_WIDTH_METADATA) + }, + iiifHeightContainer: { + iiifHeight: bitstream.firstMetadataValue(this.IMAGE_HEIGHT_METADATA) + } + }); + } this.bitstream.format.pipe( getAllSucceededRemoteDataPayload() ).subscribe((format: BitstreamFormat) => { @@ -467,6 +619,32 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { const primary = rawForm.fileNamePrimaryContainer.primaryBitstream; Metadata.setFirstValue(newMetadata, 'dc.title', rawForm.fileNamePrimaryContainer.fileName); Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description); + if (this.isIIIF) { + // It's helpful to remove these metadata elements entirely when the form value is empty. + // This avoids potential issues on the REST side and makes it possible to do things like + // remove an existing "table of contents" entry. + if (isEmpty(rawForm.iiifLabelContainer.iiifLabel)) { + + delete newMetadata[this.IIIF_LABEL_METADATA]; + } else { + Metadata.setFirstValue(newMetadata, this.IIIF_LABEL_METADATA, rawForm.iiifLabelContainer.iiifLabel); + } + if (isEmpty(rawForm.iiifTocContainer.iiifToc)) { + delete newMetadata[this.IIIF_TOC_METADATA]; + } else { + Metadata.setFirstValue(newMetadata, this.IIIF_TOC_METADATA, rawForm.iiifTocContainer.iiifToc); + } + if (isEmpty(rawForm.iiifWidthContainer.iiifWidth)) { + delete newMetadata[this.IMAGE_WIDTH_METADATA]; + } else { + Metadata.setFirstValue(newMetadata, this.IMAGE_WIDTH_METADATA, rawForm.iiifWidthContainer.iiifWidth); + } + if (isEmpty(rawForm.iiifHeightContainer.iiifHeight)) { + delete newMetadata[this.IMAGE_HEIGHT_METADATA]; + } else { + Metadata.setFirstValue(newMetadata, this.IMAGE_HEIGHT_METADATA, rawForm.iiifHeightContainer.iiifHeight); + } + } if (isNotEmpty(rawForm.formatContainer.newFormat)) { Metadata.setFirstValue(newMetadata, 'dc.format', rawForm.formatContainer.newFormat); } @@ -497,6 +675,58 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { } } + /** + * Verifies that the parent item is iiif-enabled. Checks bitstream mimetype to be + * sure it's an image, excluding bitstreams in the THUMBNAIL or OTHERCONTENT bundles. + * @param bitstream + */ + setIiifStatus(bitstream: Bitstream) { + + const regexExcludeBundles = /OTHERCONTENT|THUMBNAIL|LICENSE/; + const regexIIIFItem = /true|yes/i; + + const isImage$ = this.bitstream.format.pipe( + getFirstSucceededRemoteData(), + map((format: RemoteData) => format.payload.mimetype.includes('image/'))); + + const isIIIFBundle$ = this.bitstream.bundle.pipe( + getFirstSucceededRemoteData(), + map((bundle: RemoteData) => + this.dsoNameService.getName(bundle.payload).match(regexExcludeBundles) == null)); + + const isEnabled$ = this.bitstream.bundle.pipe( + getFirstSucceededRemoteData(), + map((bundle: RemoteData) => bundle.payload.item.pipe( + getFirstSucceededRemoteData(), + map((item: RemoteData) => + (item.payload.firstMetadataValue('dspace.iiif.enabled') && + item.payload.firstMetadataValue('dspace.iiif.enabled').match(regexIIIFItem) !== null) + )))); + + const iiifSub = combineLatest( + isImage$, + isIIIFBundle$, + isEnabled$ + ).subscribe(([isImage, isIIIFBundle, isEnabled]) => { + if (isImage && isIIIFBundle && isEnabled) { + this.isIIIF = true; + this.inputModels.push(this.iiifLabelModel); + this.formModel.push(this.iiifLabelContainer); + this.inputModels.push(this.iiifTocModel); + this.formModel.push(this.iiifTocContainer); + this.inputModels.push(this.iiifWidthModel); + this.formModel.push(this.iiifWidthContainer); + this.inputModels.push(this.iiifHeightModel); + this.formModel.push(this.iiifHeightContainer); + } + this.setForm(); + this.changeDetectorRef.detectChanges(); + }); + + this.subs.push(iiifSub); + + } + /** * Unsubscribe from open subscriptions */ diff --git a/src/app/core/auth/models/auth.method-type.ts b/src/app/core/auth/models/auth.method-type.ts index f053515065..9d999c4c3f 100644 --- a/src/app/core/auth/models/auth.method-type.ts +++ b/src/app/core/auth/models/auth.method-type.ts @@ -3,5 +3,6 @@ export enum AuthMethodType { Shibboleth = 'shibboleth', Ldap = 'ldap', Ip = 'ip', - X509 = 'x509' + X509 = 'x509', + Oidc = 'oidc' } diff --git a/src/app/core/auth/models/auth.method.ts b/src/app/core/auth/models/auth.method.ts index 617154080b..5a362e8606 100644 --- a/src/app/core/auth/models/auth.method.ts +++ b/src/app/core/auth/models/auth.method.ts @@ -29,6 +29,11 @@ export class AuthMethod { this.authMethodType = AuthMethodType.Password; break; } + case 'oidc': { + this.authMethodType = AuthMethodType.Oidc; + this.location = location; + break; + } default: { break; diff --git a/src/app/core/config/models/config-accesses-conditions-options.model.ts b/src/app/core/config/models/config-accesses-conditions-options.model.ts new file mode 100644 index 0000000000..244b501908 --- /dev/null +++ b/src/app/core/config/models/config-accesses-conditions-options.model.ts @@ -0,0 +1,45 @@ +/** + * Model class for an Item Access Condition + */ +export class AccessesConditionOption { + + /** + * The name for this Access Condition + */ + name: string; + + /** + * The groupName for this Access Condition + */ + groupName: string; + + /** + * A boolean representing if this Access Condition has a start date + */ + hasStartDate: boolean; + + /** + * A boolean representing if this Access Condition has an end date + */ + hasEndDate: boolean; + + /** + * Maximum value of the start date + */ + endDateLimit?: string; + + /** + * Maximum value of the end date + */ + startDateLimit?: string; + + /** + * Maximum value of the start date + */ + maxStartDate?: string; + + /** + * Maximum value of the end date + */ + maxEndDate?: string; +} diff --git a/src/app/core/config/models/config-submission-access.model.ts b/src/app/core/config/models/config-submission-access.model.ts new file mode 100644 index 0000000000..7db96acf2b --- /dev/null +++ b/src/app/core/config/models/config-submission-access.model.ts @@ -0,0 +1,42 @@ +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { ConfigObject } from './config.model'; +import { AccessesConditionOption } from './config-accesses-conditions-options.model'; +import { SUBMISSION_ACCESSES_TYPE } from './config-type'; +import { HALLink } from '../../shared/hal-link.model'; + +/** + * Class for the configuration describing the item accesses condition + */ +@typedObject +@inheritSerialization(ConfigObject) +export class SubmissionAccessModel extends ConfigObject { + static type = SUBMISSION_ACCESSES_TYPE; + + /** + * A list of available item access conditions + */ + @autoserialize + accessConditionOptions: AccessesConditionOption[]; + + /** + * Boolean that indicates whether the current item must be findable via search or browse. + */ + @autoserialize + discoverable: boolean; + + /** + * Boolean that indicates whether or not the user can change the discoverable flag. + */ + @autoserialize + canChangeDiscoverable: boolean; + + /** + * The links to all related resources returned by the rest api. + */ + @deserialize + _links: { + self: HALLink + }; + +} diff --git a/src/app/core/config/models/config-submission-accesses.model.ts b/src/app/core/config/models/config-submission-accesses.model.ts new file mode 100644 index 0000000000..3f8004928d --- /dev/null +++ b/src/app/core/config/models/config-submission-accesses.model.ts @@ -0,0 +1,10 @@ +import { inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; +import { SUBMISSION_ACCESSES_TYPE } from './config-type'; +import { SubmissionAccessModel } from './config-submission-access.model'; + +@typedObject +@inheritSerialization(SubmissionAccessModel) +export class SubmissionAccessesModel extends SubmissionAccessModel { + static type = SUBMISSION_ACCESSES_TYPE; +} diff --git a/src/app/core/config/models/config-type.ts b/src/app/core/config/models/config-type.ts index 858ff19c91..19864536f0 100644 --- a/src/app/core/config/models/config-type.ts +++ b/src/app/core/config/models/config-type.ts @@ -15,3 +15,5 @@ export const SUBMISSION_SECTION_TYPE = new ResourceType('submissionsection'); export const SUBMISSION_UPLOADS_TYPE = new ResourceType('submissionuploads'); export const SUBMISSION_UPLOAD_TYPE = new ResourceType('submissionupload'); + +export const SUBMISSION_ACCESSES_TYPE = new ResourceType('submissionaccessoption'); diff --git a/src/app/core/config/submission-accesses-config.service.ts b/src/app/core/config/submission-accesses-config.service.ts new file mode 100644 index 0000000000..de9afc66ea --- /dev/null +++ b/src/app/core/config/submission-accesses-config.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; +import { ConfigService } from './config.service'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { SUBMISSION_ACCESSES_TYPE } from './models/config-type'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { ConfigObject } from './models/config.model'; +import { SubmissionAccessesModel } from './models/config-submission-accesses.model'; +import { RemoteData } from '../data/remote-data'; +import { Observable } from 'rxjs'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; + +/** + * Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process. + */ +@Injectable() +@dataService(SUBMISSION_ACCESSES_TYPE) +export class SubmissionAccessesConfigService extends ConfigService { + 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(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator, 'submissionaccessoptions'); + } + + findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow): Observable> { + return super.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow as FollowLinkConfig[]) as Observable>; + } +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 2c9a24cb80..8d8a614a89 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -2,11 +2,7 @@ import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; -import { - DynamicFormLayoutService, - DynamicFormService, - DynamicFormValidationService -} from '@ng-dynamic-forms/core'; +import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { EffectsModule } from '@ngrx/effects'; import { Action, StoreConfig, StoreModule } from '@ngrx/store'; @@ -77,6 +73,7 @@ import { MetadataSchema } from './metadata/metadata-schema.model'; import { MetadataService } from './metadata/metadata.service'; import { RegistryService } from './registry/registry.service'; import { RoleService } from './roles/role.service'; +import { FeedbackDataService } from './feedback/feedback-data.service'; import { ApiService } from './services/api.service'; import { ServerResponseService } from './services/server-response.service'; @@ -164,6 +161,7 @@ import { Root } from './data/root.model'; import { SearchConfig } from './shared/search/search-filters/search-config.model'; import { SequenceService } from './shared/sequence.service'; import { GroupDataService } from './eperson/group-data.service'; +import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -286,7 +284,8 @@ const PROVIDERS = [ VocabularyService, VocabularyTreeviewService, SequenceService, - GroupDataService + GroupDataService, + FeedbackDataService, ]; /** @@ -345,7 +344,8 @@ export const models = Registration, UsageReport, Root, - SearchConfig + SearchConfig, + SubmissionAccessesModel ]; @NgModule({ diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index 01a482dc79..029c75d9cb 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -27,4 +27,5 @@ export enum FeatureID { CanDeleteVersion = 'canDeleteVersion', CanCreateVersion = 'canCreateVersion', CanViewUsageStatistics = 'canViewUsageStatistics', + CanSendFeedback = 'canSendFeedback', } diff --git a/src/app/core/feedback/feedback-data.service.spec.ts b/src/app/core/feedback/feedback-data.service.spec.ts new file mode 100644 index 0000000000..4bb5e642c2 --- /dev/null +++ b/src/app/core/feedback/feedback-data.service.spec.ts @@ -0,0 +1,88 @@ +import { FeedbackDataService } from './feedback-data.service'; +import { HALLink } from '../shared/hal-link.model'; +import { Item } from '../shared/item.model'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { Feedback } from './models/feedback.model'; + +describe('FeedbackDataService', () => { + let service: FeedbackDataService; + let requestService; + let halService; + let rdbService; + let notificationsService; + let http; + let comparator; + let objectCache; + let store; + let item; + let bundleLink; + let bundleHALLink; + + const feedbackPayload = Object.assign(new Feedback(), { + email: 'test@email.com', + message: 'message', + page: '/home' + }); + + + function initTestService(): FeedbackDataService { + bundleLink = '/items/0fdc0cd7-ff8c-433d-b33c-9b56108abc07/bundles'; + bundleHALLink = new HALLink(); + bundleHALLink.href = bundleLink; + item = new Item(); + item._links = { + bundles: bundleHALLink + }; + requestService = getMockRequestService(); + halService = new HALEndpointServiceStub('url') as any; + rdbService = {} as RemoteDataBuildService; + notificationsService = {} as NotificationsService; + http = {} as HttpClient; + comparator = new DSOChangeAnalyzer() as any; + objectCache = { + + addPatch: () => { + /* empty */ + }, + getObjectBySelfLink: () => { + /* empty */ + } + } as any; + store = {} as Store; + return new FeedbackDataService( + requestService, + rdbService, + store, + objectCache, + halService, + notificationsService, + http, + comparator, + ); + } + + + beforeEach(() => { + service = initTestService(); + }); + + + describe('getFeedback', () => { + beforeEach(() => { + spyOn(service, 'getFeedback'); + service.getFeedback('3'); + }); + + it('should call getFeedback with the feedback link', () => { + expect(service.getFeedback).toHaveBeenCalledWith('3'); + }); + }); + +}); diff --git a/src/app/core/feedback/feedback-data.service.ts b/src/app/core/feedback/feedback-data.service.ts new file mode 100644 index 0000000000..8f6d4e3ecf --- /dev/null +++ b/src/app/core/feedback/feedback-data.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { DataService } from '../data/data.service'; +import { Feedback } from './models/feedback.model'; +import { FEEDBACK } from './models/feedback.resource-type'; +import { dataService } from '../cache/builders/build-decorators'; +import { RequestService } from '../data/request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +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 { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; + +/** + * Service for checking and managing the feedback + */ +@Injectable() +@dataService(FEEDBACK) +export class FeedbackDataService extends DataService { + protected linkPath = 'feedbacks'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer, + ) { + super(); + } + + /** + * Get feedback from its id + * @param uuid string the id of the feedback + */ + getFeedback(uuid: string): Observable { + return this.findById(uuid).pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + ); + } + +} diff --git a/src/app/core/feedback/feedback.guard.ts b/src/app/core/feedback/feedback.guard.ts new file mode 100644 index 0000000000..63353a60ff --- /dev/null +++ b/src/app/core/feedback/feedback.guard.ts @@ -0,0 +1,20 @@ +import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../data/feature-authorization/feature-id'; +import { Injectable } from '@angular/core'; + +/** + * An guard for redirecting users to the feedback page if user is authorized + */ +@Injectable() +export class FeedbackGuard implements CanActivate { + + constructor(private authorizationService: AuthorizationDataService) { + } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.authorizationService.isAuthorized(FeatureID.CanSendFeedback); + } + +} diff --git a/src/app/core/feedback/models/feedback.model.ts b/src/app/core/feedback/models/feedback.model.ts new file mode 100644 index 0000000000..fd98affba9 --- /dev/null +++ b/src/app/core/feedback/models/feedback.model.ts @@ -0,0 +1,34 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { typedObject } from '../../cache/builders/build-decorators'; + +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { HALLink } from '../../shared/hal-link.model'; +import { FEEDBACK } from './feedback.resource-type'; + +@typedObject +@inheritSerialization(DSpaceObject) +export class Feedback extends DSpaceObject { + static type = FEEDBACK; + + /** + * The email address + */ + @autoserialize + public email: string; + + /** + * A string representing message the user inserted + */ + @autoserialize + public message: string; + /** + * A string representing the page from which the user came from + */ + @autoserialize + public page: string; + + _links: { + self: HALLink; + }; + +} diff --git a/src/app/core/feedback/models/feedback.resource-type.ts b/src/app/core/feedback/models/feedback.resource-type.ts new file mode 100644 index 0000000000..89b28c6cb0 --- /dev/null +++ b/src/app/core/feedback/models/feedback.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for Feedback + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const FEEDBACK = new ResourceType('feedback'); diff --git a/src/app/core/submission/models/access-condition.model.ts b/src/app/core/submission/models/access-condition.model.ts new file mode 100644 index 0000000000..5ec5c2c434 --- /dev/null +++ b/src/app/core/submission/models/access-condition.model.ts @@ -0,0 +1,25 @@ +/** + * An interface to represent an access condition. + */ +export class AccessConditionObject { + + /** + * The access condition id + */ + id: string; + + /** + * The access condition name + */ + name: string; + + /** + * Possible start date of the access condition + */ + startDate: string; + + /** + * Possible end date of the access condition + */ + endDate: string; +} diff --git a/src/app/core/submission/models/submission-accesses.resource-type.ts b/src/app/core/submission/models/submission-accesses.resource-type.ts new file mode 100644 index 0000000000..0634a63337 --- /dev/null +++ b/src/app/core/submission/models/submission-accesses.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../shared/resource-type'; + +/** + * The resource type for Accesses section + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SUBMISSION_ACCESSES = new ResourceType('submissionaccesses'); diff --git a/src/app/core/submission/models/submission-item-access-condition.model.ts b/src/app/core/submission/models/submission-item-access-condition.model.ts new file mode 100644 index 0000000000..60e749b786 --- /dev/null +++ b/src/app/core/submission/models/submission-item-access-condition.model.ts @@ -0,0 +1,8 @@ +import { AccessConditionObject } from './access-condition.model'; + +/** + * An interface to represent item's access condition. + */ +export class SubmissionItemAccessConditionObject extends AccessConditionObject { + +} diff --git a/src/app/core/submission/models/submission-upload-file-access-condition.model.ts b/src/app/core/submission/models/submission-upload-file-access-condition.model.ts index fa4d9b9062..7be9d6f1f7 100644 --- a/src/app/core/submission/models/submission-upload-file-access-condition.model.ts +++ b/src/app/core/submission/models/submission-upload-file-access-condition.model.ts @@ -1,25 +1,8 @@ +import { AccessConditionObject } from './access-condition.model'; + /** * An interface to represent bitstream's access condition. */ -export class SubmissionUploadFileAccessConditionObject { +export class SubmissionUploadFileAccessConditionObject extends AccessConditionObject { - /** - * The access condition id - */ - id: string; - - /** - * The access condition name - */ - name: string; - - /** - * Possible start date of the access condition - */ - startDate: string; - - /** - * Possible end date of the access condition - */ - endDate: string; } diff --git a/src/app/core/submission/models/workspaceitem-section-accesses.model.ts b/src/app/core/submission/models/workspaceitem-section-accesses.model.ts new file mode 100644 index 0000000000..2d571f67f8 --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-accesses.model.ts @@ -0,0 +1,21 @@ +import { SubmissionItemAccessConditionObject } from './submission-item-access-condition.model'; + +/** + * An interface to represent the submission's item accesses condition. + */ +export interface WorkspaceitemSectionAccessesObject { + /** + * The access condition id + */ + id: string; + + /** + * Boolean that indicates whether the current item must be findable via search or browse. + */ + discoverable: boolean; + + /** + * A list of available item access conditions + */ + accessConditions: SubmissionItemAccessConditionObject[]; +} diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts index 6ff756a323..084da3f088 100644 --- a/src/app/core/submission/models/workspaceitem-sections.model.ts +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -1,3 +1,4 @@ +import { WorkspaceitemSectionAccessesObject } from './workspaceitem-section-accesses.model'; import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model'; import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model'; import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model'; @@ -19,4 +20,5 @@ export type WorkspaceitemSectionDataType | WorkspaceitemSectionFormObject | WorkspaceitemSectionLicenseObject | WorkspaceitemSectionCcLicenseObject + | WorkspaceitemSectionAccessesObject | string; diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index c8ee8a2f5b..2c1a34ccae 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -75,6 +75,10 @@ {{ 'footer.link.end-user-agreement' | translate}}
  • +
  • + {{ 'footer.link.feedback' | translate}} +
diff --git a/src/app/info/feedback/feedback-form/feedback-form.component.html b/src/app/info/feedback/feedback-form/feedback-form.component.html new file mode 100644 index 0000000000..02745f2580 --- /dev/null +++ b/src/app/info/feedback/feedback-form/feedback-form.component.html @@ -0,0 +1,45 @@ +
+
+
+

{{ 'info.feedback.head' | translate }}

+

{{ 'info.feedback.info' | translate }}

+
+
+
+ + + {{ 'info.feedback.email_help' | translate }} +
+
+ + + + + +
+
+ + +
+
+ + + +
+
+ + + {{ 'info.feedback.page_help' | translate }} +
+
+
+
+ +
+
+
+
+
+
diff --git a/src/app/info/feedback/feedback-form/feedback-form.component.scss b/src/app/info/feedback/feedback-form/feedback-form.component.scss new file mode 100644 index 0000000000..41d439e591 --- /dev/null +++ b/src/app/info/feedback/feedback-form/feedback-form.component.scss @@ -0,0 +1,3 @@ +ds-error{ + color:red; +} \ No newline at end of file diff --git a/src/app/info/feedback/feedback-form/feedback-form.component.spec.ts b/src/app/info/feedback/feedback-form/feedback-form.component.spec.ts new file mode 100644 index 0000000000..d6bedc46cf --- /dev/null +++ b/src/app/info/feedback/feedback-form/feedback-form.component.spec.ts @@ -0,0 +1,97 @@ +import { EPersonMock } from '../../../shared/testing/eperson.mock'; +import { FeedbackDataService } from '../../../core/feedback/feedback-data.service'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FeedbackFormComponent } from './feedback-form.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { RouteService } from '../../../core/services/route.service'; +import { routeServiceStub } from '../../../shared/testing/route-service.stub'; +import { FormBuilder } from '@angular/forms'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { AuthService } from '../../../core/auth/auth.service'; +import { AuthServiceStub } from '../../../shared/testing/auth-service.stub'; +import { of } from 'rxjs'; +import { Feedback } from '../../../core/feedback/models/feedback.model'; +import { Router } from '@angular/router'; +import { RouterMock } from '../../../shared/mocks/router.mock'; +import { NativeWindowService } from '../../../core/services/window.service'; +import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref'; + + +describe('FeedbackFormComponent', () => { + let component: FeedbackFormComponent; + let fixture: ComponentFixture; + let de: DebugElement; + const notificationService = new NotificationsServiceStub(); + const feedbackDataServiceStub = jasmine.createSpyObj('feedbackDataService', { + create: of(new Feedback()) + }); + const authService: AuthServiceStub = Object.assign(new AuthServiceStub(), { + getAuthenticatedUserFromStore: () => { + return of(EPersonMock); + } + }); + const routerStub = new RouterMock(); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [FeedbackFormComponent], + providers: [ + { provide: RouteService, useValue: routeServiceStub }, + { provide: FormBuilder, useValue: new FormBuilder() }, + { provide: NotificationsService, useValue: notificationService }, + { provide: FeedbackDataService, useValue: feedbackDataServiceStub }, + { provide: AuthService, useValue: authService }, + { provide: NativeWindowService, useFactory: NativeWindowMockFactory }, + { provide: Router, useValue: routerStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FeedbackFormComponent); + component = fixture.componentInstance; + de = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have page value', () => { + expect(component.feedbackForm.controls.page.value).toEqual('http://localhost/home'); + }); + + it('should have email if ePerson', () => { + expect(component.feedbackForm.controls.email.value).toEqual('test@test.com'); + }); + + it('should have disabled button', () => { + expect(de.query(By.css('button')).nativeElement.disabled).toBeTrue(); + }); + + describe('when message is inserted', () => { + + beforeEach(() => { + component.feedbackForm.patchValue({ message: 'new feedback' }); + fixture.detectChanges(); + }); + + it('should not have disabled button', () => { + expect(de.query(By.css('button')).nativeElement.disabled).toBeFalse(); + }); + + it('on submit should call createFeedback of feedbackDataServiceStub service', () => { + component.createFeedback(); + fixture.detectChanges(); + expect(feedbackDataServiceStub.create).toHaveBeenCalled(); + }); + }); + + +}); diff --git a/src/app/info/feedback/feedback-form/feedback-form.component.ts b/src/app/info/feedback/feedback-form/feedback-form.component.ts new file mode 100644 index 0000000000..35cdd69cfc --- /dev/null +++ b/src/app/info/feedback/feedback-form/feedback-form.component.ts @@ -0,0 +1,83 @@ +import { RemoteData } from '../../../core/data/remote-data'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { FeedbackDataService } from '../../../core/feedback/feedback-data.service'; +import { Component, Inject, OnInit } from '@angular/core'; +import { RouteService } from '../../../core/services/route.service'; +import { FormBuilder, Validators } from '@angular/forms'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { AuthService } from '../../../core/auth/auth.service'; +import { EPerson } from '../../../core/eperson/models/eperson.model'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { Router } from '@angular/router'; +import { getHomePageRoute } from '../../../app-routing-paths'; +import { take } from 'rxjs/operators'; +import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; + +@Component({ + selector: 'ds-feedback-form', + templateUrl: './feedback-form.component.html', + styleUrls: ['./feedback-form.component.scss'] +}) +/** + * Component displaying the contents of the Feedback Statement + */ +export class FeedbackFormComponent implements OnInit { + + /** + * Form builder created used from the feedback from + */ + feedbackForm = this.fb.group({ + email: ['', [Validators.required, Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$')]], + message: ['', Validators.required], + page: [''], + }); + + constructor( + @Inject(NativeWindowService) protected _window: NativeWindowRef, + public routeService: RouteService, + private fb: FormBuilder, + protected notificationsService: NotificationsService, + protected translate: TranslateService, + private feedbackDataService: FeedbackDataService, + private authService: AuthService, + private router: Router) { + } + + /** + * On init check if user is logged in and use its email if so + */ + ngOnInit() { + + this.authService.getAuthenticatedUserFromStore().pipe(take(1)).subscribe((user: EPerson) => { + if (!!user) { + this.feedbackForm.patchValue({ email: user.email }); + } + }); + + this.routeService.getPreviousUrl().pipe(take(1)).subscribe((url: string) => { + if (!url) { + url = getHomePageRoute(); + } + const relatedUrl = new URLCombiner(this._window.nativeWindow.origin, url).toString(); + this.feedbackForm.patchValue({ page: relatedUrl }); + }); + + } + + /** + * Function to create the feedback from form values + */ + createFeedback(): void { + const url = this.feedbackForm.value.page.replace(this._window.nativeWindow.origin, ''); + this.feedbackDataService.create(this.feedbackForm.value).pipe(getFirstCompletedRemoteData()).subscribe((response: RemoteData) => { + if (response.isSuccess) { + this.notificationsService.success(this.translate.instant('info.feedback.create.success')); + this.feedbackForm.reset(); + this.router.navigateByUrl(url); + } + }); + } + +} diff --git a/src/app/info/feedback/feedback.component.html b/src/app/info/feedback/feedback.component.html new file mode 100644 index 0000000000..210bdcf1d7 --- /dev/null +++ b/src/app/info/feedback/feedback.component.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/app/info/feedback/feedback.component.scss b/src/app/info/feedback/feedback.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/info/feedback/feedback.component.spec.ts b/src/app/info/feedback/feedback.component.spec.ts new file mode 100644 index 0000000000..810c3b703f --- /dev/null +++ b/src/app/info/feedback/feedback.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FeedbackComponent } from './feedback.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('FeedbackComponent', () => { + let component: FeedbackComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [FeedbackComponent], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FeedbackComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/info/feedback/feedback.component.ts b/src/app/info/feedback/feedback.component.ts new file mode 100644 index 0000000000..3c0924b4de --- /dev/null +++ b/src/app/info/feedback/feedback.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-feedback', + templateUrl: './feedback.component.html', + styleUrls: ['./feedback.component.scss'] +}) +/** + * Component displaying the Feedback Statement + */ +export class FeedbackComponent { +} diff --git a/src/app/info/feedback/themed-feedback.component.ts b/src/app/info/feedback/themed-feedback.component.ts new file mode 100644 index 0000000000..68581c32fd --- /dev/null +++ b/src/app/info/feedback/themed-feedback.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { FeedbackComponent } from './feedback.component'; + +/** + * Themed wrapper for FeedbackComponent + */ +@Component({ + selector: 'ds-themed-feedback', + styleUrls: [], + templateUrl: '../../shared/theme-support/themed.component.html', +}) +export class ThemedFeedbackComponent extends ThemedComponent { + protected getComponentName(): string { + return 'FeedbackComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/info/feedback/feedback.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./feedback.component`); + } + +} diff --git a/src/app/info/info-routing-paths.ts b/src/app/info/info-routing-paths.ts index 8ec6dbcb8d..a18de2c611 100644 --- a/src/app/info/info-routing-paths.ts +++ b/src/app/info/info-routing-paths.ts @@ -2,6 +2,7 @@ import { getInfoModulePath } from '../app-routing-paths'; export const END_USER_AGREEMENT_PATH = 'end-user-agreement'; export const PRIVACY_PATH = 'privacy'; +export const FEEDBACK_PATH = 'feedback'; export function getEndUserAgreementPath() { return getSubPath(END_USER_AGREEMENT_PATH); @@ -11,6 +12,10 @@ export function getPrivacyPath() { return getSubPath(PRIVACY_PATH); } +export function getFeedbackPath() { + return getSubPath(FEEDBACK_PATH); +} + function getSubPath(path: string) { return `${getInfoModulePath()}/${path}`; } diff --git a/src/app/info/info-routing.module.ts b/src/app/info/info-routing.module.ts index f76fb47ff0..c746bebaaa 100644 --- a/src/app/info/info-routing.module.ts +++ b/src/app/info/info-routing.module.ts @@ -1,9 +1,12 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { PRIVACY_PATH, END_USER_AGREEMENT_PATH } from './info-routing-paths'; +import { PRIVACY_PATH, END_USER_AGREEMENT_PATH, FEEDBACK_PATH } from './info-routing-paths'; import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component'; import { ThemedPrivacyComponent } from './privacy/themed-privacy.component'; +import { ThemedFeedbackComponent } from './feedback/themed-feedback.component'; +import { FeedbackGuard } from '../core/feedback/feedback.guard'; + @NgModule({ imports: [ @@ -22,6 +25,15 @@ import { ThemedPrivacyComponent } from './privacy/themed-privacy.component'; resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' } } + ]), + RouterModule.forChild([ + { + path: FEEDBACK_PATH, + component: ThemedFeedbackComponent, + resolve: { breadcrumb: I18nBreadcrumbResolver }, + data: { title: 'info.feedback.title', breadcrumbKey: 'info.feedback' }, + canActivate: [FeedbackGuard] + } ]) ] }) diff --git a/src/app/info/info.module.ts b/src/app/info/info.module.ts index 84ba5a62db..61eee71f3a 100644 --- a/src/app/info/info.module.ts +++ b/src/app/info/info.module.ts @@ -8,6 +8,11 @@ import { PrivacyComponent } from './privacy/privacy.component'; import { PrivacyContentComponent } from './privacy/privacy-content/privacy-content.component'; import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component'; import { ThemedPrivacyComponent } from './privacy/themed-privacy.component'; +import { FeedbackComponent } from './feedback/feedback.component'; +import { FeedbackFormComponent } from './feedback/feedback-form/feedback-form.component'; +import { ThemedFeedbackComponent } from './feedback/themed-feedback.component'; +import { FeedbackGuard } from '../core/feedback/feedback.guard'; + const DECLARATIONS = [ EndUserAgreementComponent, @@ -15,21 +20,25 @@ const DECLARATIONS = [ EndUserAgreementContentComponent, PrivacyComponent, PrivacyContentComponent, - ThemedPrivacyComponent + ThemedPrivacyComponent, + FeedbackComponent, + FeedbackFormComponent, + ThemedFeedbackComponent ]; @NgModule({ imports: [ CommonModule, SharedModule, - InfoRoutingModule + InfoRoutingModule, ], declarations: [ ...DECLARATIONS ], exports: [ ...DECLARATIONS - ] + ], + providers: [FeedbackGuard] }) export class InfoModule { } diff --git a/src/app/item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/item-page/edit-item-page/item-delete/item-delete.component.spec.ts index ea78767df5..2533de32b2 100644 --- a/src/app/item-page/edit-item-page/item-delete/item-delete.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-delete/item-delete.component.spec.ts @@ -3,7 +3,7 @@ import { ItemType } from '../../../core/shared/item-relationships/item-type.mode import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../core/shared/item.model'; import { RouterStub } from '../../../shared/testing/router.stub'; -import { of as observableOf } from 'rxjs'; +import { of as observableOf, EMPTY } from 'rxjs'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -24,6 +24,8 @@ import { RelationshipType } from '../../../core/shared/item-relationships/relati import { EntityTypeService } from '../../../core/data/entity-type.service'; import { getItemEditRoute } from '../../item-page-routing-paths'; import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { RelationshipTypeService } from '../../../core/data/relationship-type.service'; +import { LinkService } from '../../../core/cache/builders/link.service'; let comp: ItemDeleteComponent; let fixture: ComponentFixture; @@ -40,6 +42,7 @@ let mockItemDataService: ItemDataService; let routeStub; let objectUpdatesServiceStub; let relationshipService; +let linkService; let entityTypeService; let notificationsServiceStub; let typesSelection; @@ -52,7 +55,12 @@ describe('ItemDeleteComponent', () => { uuid: 'fake-uuid', handle: 'fake/handle', lastModified: '2018', - isWithdrawn: true + isWithdrawn: true, + metadata: { + 'dspace.entity.type': [ + { value: 'Person' } + ] + } }); itemType = Object.assign(new ItemType(), { @@ -129,6 +137,12 @@ describe('ItemDeleteComponent', () => { } ); + linkService = jasmine.createSpyObj('linkService', + { + resolveLinks: relationships[0], + } + ); + notificationsServiceStub = new NotificationsServiceStub(); TestBed.configureTestingModule({ @@ -142,6 +156,8 @@ describe('ItemDeleteComponent', () => { { provide: ObjectUpdatesService, useValue: objectUpdatesServiceStub }, { provide: RelationshipService, useValue: relationshipService }, { provide: EntityTypeService, useValue: entityTypeService }, + { provide: RelationshipTypeService, useValue: {} }, + { provide: LinkService, useValue: linkService }, ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ] @@ -166,25 +182,45 @@ describe('ItemDeleteComponent', () => { }); describe('performAction', () => { - it('should call delete function from the ItemDataService', () => { - spyOn(comp, 'notify'); - comp.performAction(); - expect(mockItemDataService.delete) - .toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id)); - expect(comp.notify).toHaveBeenCalled(); + describe(`when there are entitytypes`, () => { + it('should call delete function from the ItemDataService', () => { + spyOn(comp, 'notify'); + comp.performAction(); + expect(mockItemDataService.delete) + .toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id)); + expect(comp.notify).toHaveBeenCalled(); + }); + + it('should call delete function from the ItemDataService with empty types', () => { + + spyOn(comp, 'notify'); + jasmine.getEnv().allowRespy(true); + spyOn(entityTypeService, 'getEntityTypeRelationships').and.returnValue([]); + comp.ngOnInit(); + + comp.performAction(); + + expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id, []); + expect(comp.notify).toHaveBeenCalled(); + }); }); - it('should call delete function from the ItemDataService with empty types', () => { + describe(`when there are no entity types`, () => { + beforeEach(() => { + (comp as any).entityTypeService = jasmine.createSpyObj('entityTypeService', + { + getEntityTypeByLabel: EMPTY, + } + ); + }); - spyOn(comp, 'notify'); - jasmine.getEnv().allowRespy(true); - spyOn(entityTypeService, 'getEntityTypeRelationships').and.returnValue([]); - comp.ngOnInit(); - - comp.performAction(); - - expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id, []); - expect(comp.notify).toHaveBeenCalled(); + it('should call delete function from the ItemDataService', () => { + spyOn(comp, 'notify'); + comp.performAction(); + expect(mockItemDataService.delete) + .toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id)); + expect(comp.notify).toHaveBeenCalled(); + }); }); }); describe('notify', () => { diff --git a/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts index 0249422c8e..7735fae0ea 100644 --- a/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts @@ -1,12 +1,14 @@ -import { Component, Input, OnInit } from '@angular/core'; -import {defaultIfEmpty, filter, map, switchMap, take} from 'rxjs/operators'; -import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; +import { Component, Input, OnInit, OnDestroy } from '@angular/core'; +import { defaultIfEmpty, filter, map, switchMap, take } from 'rxjs/operators'; +import { + AbstractSimpleItemActionComponent +} from '../simple-item-action/abstract-simple-item-action.component'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { combineLatest as observableCombineLatest, combineLatest, Observable, - of as observableOf + of as observableOf, Subscription } from 'rxjs'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component'; @@ -32,6 +34,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model'; import { getItemEditRoute } from '../../item-page-routing-paths'; import { RemoteData } from '../../../core/data/remote-data'; import { NoContent } from '../../../core/shared/NoContent.model'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; @Component({ selector: 'ds-item-delete', @@ -42,7 +45,7 @@ import { NoContent } from '../../../core/shared/NoContent.model'; */ export class ItemDeleteComponent extends AbstractSimpleItemActionComponent - implements OnInit { + implements OnInit, OnDestroy { /** * The current url of this page @@ -60,7 +63,7 @@ export class ItemDeleteComponent * A list of the relationship types for which this item has relations as an observable. * The list doesn't contain duplicates. */ - types$: Observable; + types$: BehaviorSubject = new BehaviorSubject([]); /** * A map which stores the relationships of this item for each type as observable lists @@ -84,6 +87,11 @@ export class ItemDeleteComponent */ public modalRef: NgbModalRef; + /** + * Array to track all subscriptions and unsubscribe them onDestroy + */ + private subs: Subscription[] = []; + constructor(protected route: ActivatedRoute, protected router: Router, protected notificationsService: NotificationsService, @@ -113,8 +121,8 @@ export class ItemDeleteComponent this.url = this.router.url; const label = this.item.firstMetadataValue('dspace.entity.type'); - if (label !== undefined) { - this.types$ = this.entityTypeService.getEntityTypeByLabel(label).pipe( + if (isNotEmpty(label)) { + this.subs.push(this.entityTypeService.getEntityTypeByLabel(label).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)), @@ -138,16 +146,14 @@ export class ItemDeleteComponent ), ); }) - ); - } else { - this.types$ = observableOf([]); + ).subscribe((types: RelationshipType[]) => this.types$.next(types))); } - this.types$.pipe( + this.subs.push(this.types$.pipe( take(1), ).subscribe((types) => this.objectUpdatesService.initialize(this.url, types, this.item.lastModified) - ); + )); } /** @@ -327,7 +333,7 @@ export class ItemDeleteComponent */ performAction() { - this.types$.pipe( + this.subs.push(this.types$.pipe( switchMap((types) => combineLatest( types.map((type) => this.isSelected(type)) @@ -339,13 +345,14 @@ export class ItemDeleteComponent map((selectedTypes) => selectedTypes.map((type) => type.id)), ) ), - ).subscribe((types) => { - this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData()).subscribe( - (rd: RemoteData) => { - this.notify(rd.hasSucceeded); - } - ); - }); + switchMap((types) => + this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData()) + ) + ).subscribe( + (rd: RemoteData) => { + this.notify(rd.hasSucceeded); + } + )); } /** @@ -361,4 +368,14 @@ export class ItemDeleteComponent this.router.navigate([getItemEditRoute(this.item)]); } } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index fc115e043a..55e354ea7a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -14,7 +14,8 @@
-
-
- +
+
@@ -24,7 +26,7 @@

{{'submission.general.discard.confirm.info' | translate}}

diff --git a/src/app/shared/number-picker/number-picker.component.html b/src/app/shared/number-picker/number-picker.component.html index 58b6def50b..9b2ef98925 100644 --- a/src/app/shared/number-picker/number-picker.component.html +++ b/src/app/shared/number-picker/number-picker.component.html @@ -9,6 +9,7 @@ Increment
From 8c865b758eba2847056154a3bb20fb38a5bb2523 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 9 Feb 2022 15:07:08 -0600 Subject: [PATCH 063/435] Fix bugs / stability issues with search e2e tests --- cypress/integration/search-navbar.spec.ts | 6 +++--- cypress/integration/search-page.spec.ts | 18 +++++++----------- .../object-grid/object-grid.component.html | 2 +- .../object-list/object-list.component.html | 2 +- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/cypress/integration/search-navbar.spec.ts b/cypress/integration/search-navbar.spec.ts index 3aa8e7248d..211b4836b1 100644 --- a/cypress/integration/search-navbar.spec.ts +++ b/cypress/integration/search-navbar.spec.ts @@ -31,7 +31,7 @@ describe('Search from Navigation Bar', () => { // Wait for search results to come back from the above GET command cy.wait('@search-results'); // At least one search result should be displayed - cy.get('ds-item-search-result-list-element').should('be.visible'); + cy.get('[data-e2e="list-object"]').should('be.visible'); }); it('should go to search page with correct query if submitted (from search)', () => { @@ -46,7 +46,7 @@ describe('Search from Navigation Bar', () => { // Wait for search results to come back from the above GET command cy.wait('@search-results'); // At least one search result should be displayed - cy.get('ds-item-search-result-list-element').should('be.visible'); + cy.get('[data-e2e="list-object"]').should('be.visible'); }); it('should allow user to also submit query by clicking icon', () => { @@ -61,6 +61,6 @@ describe('Search from Navigation Bar', () => { // Wait for search results to come back from the above GET command cy.wait('@search-results'); // At least one search result should be displayed - cy.get('ds-item-search-result-list-element').should('be.visible'); + cy.get('[data-e2e="list-object"]').should('be.visible'); }); }); diff --git a/cypress/integration/search-page.spec.ts b/cypress/integration/search-page.spec.ts index cbd518b97c..981c9365aa 100644 --- a/cypress/integration/search-page.spec.ts +++ b/cypress/integration/search-page.spec.ts @@ -1,13 +1,8 @@ import { Options } from 'cypress-axe'; +import { TEST_SEARCH_TERM } from 'cypress/support'; import { testA11y } from 'cypress/support/utils'; describe('Search Page', () => { - it('should contain query value when navigating to page with query parameter', () => { - const queryString = 'test query'; - cy.visit('/search?query=' + queryString); - cy.get('[data-e2e="search-box"]').should('have.value', queryString); - }); - it('should redirect to the correct url when query was set and submit button was triggered', () => { const queryString = 'Another interesting query string'; cy.visit('/search'); @@ -18,13 +13,14 @@ describe('Search Page', () => { }); it('should load results and pass accessibility tests', () => { - cy.visit('/search'); + cy.visit('/search?query=' + TEST_SEARCH_TERM); + cy.get('[data-e2e="search-box"]').should('have.value', TEST_SEARCH_TERM); // tag must be loaded cy.get('ds-search-page').should('exist'); // At least one search result should be displayed - cy.get('ds-item-search-result-list-element').should('be.visible'); + cy.get('[data-e2e="list-object"]').should('be.visible'); // Click each filter toggle to open *every* filter // (As we want to scan filter section for accessibility issues as well) @@ -49,7 +45,7 @@ describe('Search Page', () => { }); it('should have a working grid view that passes accessibility tests', () => { - cy.visit('/search'); + cy.visit('/search?query=' + TEST_SEARCH_TERM); // Click button in sidebar to display grid view cy.get('ds-search-sidebar [data-e2e="grid-view"]').click(); @@ -57,8 +53,8 @@ describe('Search Page', () => { // tag must be loaded cy.get('ds-search-page').should('exist'); - // At least one grid element (card) should be displayed - cy.get('ds-item-search-result-grid-element').should('be.visible'); + // At least one grid object (card) should be displayed + cy.get('[data-e2e="grid-object"]').should('be.visible'); // Analyze for accessibility issues testA11y('ds-search-page', diff --git a/src/app/shared/object-grid/object-grid.component.html b/src/app/shared/object-grid/object-grid.component.html index 0afd623d86..296f190d2d 100644 --- a/src/app/shared/object-grid/object-grid.component.html +++ b/src/app/shared/object-grid/object-grid.component.html @@ -13,7 +13,7 @@ (paginationChange)="onPaginationChange($event)">
-
+
diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index 331ff1cb28..53a62ca1f5 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -12,7 +12,7 @@ (sortFieldChange)="onSortFieldChange($event)" (paginationChange)="onPaginationChange($event)">
    -
  • +
  • Date: Mon, 14 Feb 2022 11:28:08 -0600 Subject: [PATCH 064/435] Update submission tests to use Demo Submitter. Switch Docker scripts to use updated `dspace7-entities-data.sql` which now includes demo in-progress submissions --- cypress.json | 4 +++- cypress/integration/my-dspace.spec.ts | 22 ++++++++++++++-------- cypress/integration/submission.spec.ts | 10 +++++----- cypress/support/index.ts | 2 ++ docker/cli.assetstore.yml | 2 +- docker/db.entities.yml | 2 +- docker/docker-compose-ci.yml | 2 +- 7 files changed, 27 insertions(+), 17 deletions(-) diff --git a/cypress.json b/cypress.json index fe74ce3ff9..c0b5a56a15 100644 --- a/cypress.json +++ b/cypress.json @@ -19,6 +19,8 @@ "DSPACE_TEST_ENTITY_PUBLICATION": "e98b0f27-5c19-49a0-960d-eb6ad5287067", "DSPACE_TEST_SEARCH_TERM": "test", "DSPACE_TEST_SUBMIT_COLLECTION_NAME": "Sample Collection", - "DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144" + "DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144", + "DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com", + "DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace" } } \ No newline at end of file diff --git a/cypress/integration/my-dspace.spec.ts b/cypress/integration/my-dspace.spec.ts index 6222d30cfb..da78ff8da6 100644 --- a/cypress/integration/my-dspace.spec.ts +++ b/cypress/integration/my-dspace.spec.ts @@ -1,17 +1,17 @@ import { Options } from 'cypress-axe'; -import { TEST_ADMIN_USER, TEST_ADMIN_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support'; +import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support'; import { testA11y } from 'cypress/support/utils'; describe('My DSpace page', () => { it('should display recent submissions and pass accessibility tests', () => { - cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); cy.visit('/mydspace'); cy.get('ds-my-dspace-page').should('exist'); // At least one recent submission should be displayed - cy.get('ds-item-search-result-list-element-submission').should('be.visible'); + cy.get('[data-e2e="list-object"]').should('be.visible'); // Click each filter toggle to open *every* filter // (As we want to scan filter section for accessibility issues as well) @@ -36,7 +36,7 @@ describe('My DSpace page', () => { }); it('should have a working detailed view that passes accessibility tests', () => { - cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); cy.visit('/mydspace'); @@ -61,7 +61,7 @@ describe('My DSpace page', () => { // NOTE: Deleting existing submissions is exercised by submission.spec.ts it('should let you start a new submission & edit in-progress submissions', () => { - cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); cy.visit('/mydspace'); // Open the New Submission dropdown @@ -97,8 +97,14 @@ describe('My DSpace page', () => { // Part 2 will be the [id] of the submission const id = subpaths[2]; - // Go back to the MyDSpace page - cy.visit('/mydspace'); + // Click the "Save for Later" button to save this submission + cy.get('button#saveForLater').click(); + + // "Save for Later" should send us to MyDSpace + cy.url().should('include', '/mydspace'); + + // Close any open notifications, to make sure they don't get in the way of next steps + cy.get('[data-dismiss="alert"]').click({multiple: true}); // This is the GET command that will actually run the search cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); @@ -125,7 +131,7 @@ describe('My DSpace page', () => { }); it('should let you import from external sources', () => { - cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); cy.visit('/mydspace'); // Open the New Import dropdown diff --git a/cypress/integration/submission.spec.ts b/cypress/integration/submission.spec.ts index 9469ee35e1..4c7758f8d6 100644 --- a/cypress/integration/submission.spec.ts +++ b/cypress/integration/submission.spec.ts @@ -1,12 +1,12 @@ import { Options } from 'cypress-axe'; -import { TEST_ADMIN_USER, TEST_ADMIN_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support'; +import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support'; import { testA11y } from 'cypress/support/utils'; describe('New Submission page', () => { // NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts - it('should create a new submission when using /submit path', () => { - cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + it('should create a new submission when using /submit path & pass accessibility', () => { + cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); // Test that calling /submit with collection & entityType will create a new submission cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); @@ -33,7 +33,7 @@ describe('New Submission page', () => { }); it('should block submission & show errors if required fields are missing', () => { - cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); // Create a new submission cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); @@ -91,7 +91,7 @@ describe('New Submission page', () => { }); it('should allow for deposit if all required fields completed & file uploaded', () => { - cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); // Create a new submission cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index a56ace7d38..8edfdedde3 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -54,3 +54,5 @@ export const TEST_SEARCH_TERM = Cypress.env('DSPACE_TEST_SEARCH_TERM') || 'test' // Collection used for submission tests export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME') || 'Sample Collection'; export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144'; +export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com'; +export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace'; diff --git a/docker/cli.assetstore.yml b/docker/cli.assetstore.yml index c2846286d7..40e4974c7c 100644 --- a/docker/cli.assetstore.yml +++ b/docker/cli.assetstore.yml @@ -35,6 +35,6 @@ services: tar xvfz /tmp/assetstore.tar.gz fi - /dspace/bin/dspace index-discovery + /dspace/bin/dspace index-discovery -b /dspace/bin/dspace oai import /dspace/bin/dspace oai clean-cache diff --git a/docker/db.entities.yml b/docker/db.entities.yml index 818d14877c..d1dfdf4a26 100644 --- a/docker/db.entities.yml +++ b/docker/db.entities.yml @@ -20,7 +20,7 @@ services: environment: # This LOADSQL should be kept in sync with the URL in DSpace/DSpace # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql + - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql dspace: ### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' #### # Ensure that the database is ready BEFORE starting tomcat diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index a895314a17..3bd8f52630 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -63,7 +63,7 @@ services: # This LOADSQL should be kept in sync with the LOADSQL in # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql + LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql PGDATA: /pgdata image: dspace/dspace-postgres-pgcrypto:loadsql networks: From 852da27c2c9e0ac8bebe0a4310d54e55bd934020 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 4 Mar 2022 12:49:55 -0600 Subject: [PATCH 065/435] Update Cypress to read REST API URL dynamically from config.json --- cypress.json | 1 - cypress/support/commands.ts | 69 +++++++++++++++++++++++-------------- cypress/support/index.ts | 7 +++- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/cypress.json b/cypress.json index c0b5a56a15..80358eb6dd 100644 --- a/cypress.json +++ b/cypress.json @@ -11,7 +11,6 @@ "openMode": 0 }, "env": { - "DSPACE_TEST_REST_BASE_URL": "http://localhost:8080", "DSPACE_TEST_ADMIN_USER": "dspacedemo+admin@gmail.com", "DSPACE_TEST_ADMIN_PASSWORD": "dspace", "DSPACE_TEST_COMMUNITY": "0958c910-2037-42a9-81c7-dca80e3892b4", diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index dcac5aaaac..d66e1ef0bd 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -4,7 +4,7 @@ // *********************************************** import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; -import { TEST_REST_BASE_URL } from '.'; +import { FALLBACK_TEST_REST_BASE_URL } from '.'; // Declare Cypress namespace to help with Intellisense & code completion in IDEs // ALL custom commands MUST be listed here for code completion to work @@ -30,34 +30,53 @@ declare global { * @param password password to login as */ function login(email: string, password: string): void { - // To login via REST, first we have to do a GET to obtain a valid CSRF token - cy.request( TEST_REST_BASE_URL + '/server/api/authn/status' ) - .then((response) => { - // We should receive a CSRF token returned in a response header - expect(response.headers).to.have.property('dspace-xsrf-token'); - const csrfToken = response.headers['dspace-xsrf-token']; + // Cypress doesn't have access to the running application in Node.js. + // So, it's not possible to inject or load the AppConfig or environment of the Angular UI. + // Instead, we'll read our running application's config.json, which contains the configs & + // is regenerated at runtime each time the Angular UI application starts up. + cy.readFile('./src/assets/config.json').then((str) => { + // Parse JSON file into a JSON object + const config = JSON.parse(JSON.stringify(str)); - // Now, send login POST request including that CSRF token - cy.request({ - method: 'POST', - url: TEST_REST_BASE_URL + '/server/api/authn/login', - headers: { 'X-XSRF-TOKEN' : csrfToken}, - form: true, // indicates the body should be form urlencoded - body: { user: email, password: password } - }).then((resp) => { - // We expect a successful login - expect(resp.status).to.eq(200); - // We expect to have a valid authorization header returned (with our auth token) - expect(resp.headers).to.have.property('authorization'); + // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found. + let baseRestUrl = FALLBACK_TEST_REST_BASE_URL; + if (!config.rest.baseUrl) { + console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL); + } else { + console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: " + config.rest.baseUrl); + baseRestUrl = config.rest.baseUrl; + } - // Initialize our AuthTokenInfo object from the authorization header. - const authheader = resp.headers.authorization as string; - const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader); + // To login via REST, first we have to do a GET to obtain a valid CSRF token + cy.request( baseRestUrl + '/api/authn/status' ) + .then((response) => { + // We should receive a CSRF token returned in a response header + expect(response.headers).to.have.property('dspace-xsrf-token'); + const csrfToken = response.headers['dspace-xsrf-token']; - // Save our AuthTokenInfo object to our dsAuthInfo UI cookie - // This ensures the UI will recognize we are logged in on next "visit()" - cy.setCookie(TOKENITEM, JSON.stringify(authinfo)); + // Now, send login POST request including that CSRF token + cy.request({ + method: 'POST', + url: baseRestUrl + '/api/authn/login', + headers: { 'X-XSRF-TOKEN' : csrfToken}, + form: true, // indicates the body should be form urlencoded + body: { user: email, password: password } + }).then((resp) => { + // We expect a successful login + expect(resp.status).to.eq(200); + // We expect to have a valid authorization header returned (with our auth token) + expect(resp.headers).to.have.property('authorization'); + + // Initialize our AuthTokenInfo object from the authorization header. + const authheader = resp.headers.authorization as string; + const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader); + + // Save our AuthTokenInfo object to our dsAuthInfo UI cookie + // This ensures the UI will recognize we are logged in on next "visit()" + cy.setCookie(TOKENITEM, JSON.stringify(authinfo)); + }); }); + }); } // Add as a Cypress command (i.e. assign to 'cy.login') diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 8edfdedde3..d9b6409a0d 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -42,7 +42,12 @@ afterEach(() => { // Default values listed here are all valid for the Demo Entities Data set available at // https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data // (This is the data set used in our CI environment) -export const TEST_REST_BASE_URL = Cypress.env('DSPACE_TEST_REST_BASE_URL') || 'http://localhost:8080'; + +// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL +// from the Angular UI's config.json. See 'getBaseRESTUrl()' in commands.ts +export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server'; + +// Admin account used for administrative tests export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com'; export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace'; // Community/collection/publication used for view/edit tests From a0e2ac3d122bab4bb3dd092aa73195db154ca930 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 4 Mar 2022 12:50:21 -0600 Subject: [PATCH 066/435] Update README with clearer instructions on running 'ng e2e' --- README.md | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 74010f3c5c..d9c911d30a 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ After you have installed all dependencies you can now run the app. Run `yarn run When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload. -To build the app for production and start the server run: +To build the app for production and start the server (in one command) run: ```bash yarn start @@ -207,6 +207,10 @@ yarn run build:prod ``` This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`. +After building the app for production, it can be started by running: +```bash +yarn run serve:ssr +``` ### Running the application with Docker NOTE: At this time, we do not have production-ready Docker images for DSpace. @@ -268,11 +272,29 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con The test files can be found in the `./cypress/integration/` folder. -Before you can run e2e tests, two things are required: -1. You MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `config.prod.yml` or `config.yml`. You may override this using env variables, see [Configuring](#configuring). -2. Your backend MUST include our Entities Test Data set. Some tests run against a (currently hardcoded) Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set. The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data +Before you can run e2e tests, two things are REQUIRED: +1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time. + * After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend. + * If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example: + ``` + DSPACE_REST_SSL = false + DSPACE_REST_HOST = localhost + DSPACE_REST_PORT = 8080 + ``` +2. Your backend MUST include our [Entities Test Data set](https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data). Some tests run against a specific Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set. + * (Recommended) The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data + * Alternatively, the Entities Test Data set may be installed via a simple SQL import (e. g. `psql -U dspace < dspace7-entities-data.sql`). See instructions in link above. -Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results. +After performing the above setup, you can run the e2e tests using +``` +ng e2e +```` +NOTE: By default these tests will run against the REST API backend configured via environment variables or in `config.prod.yml`. If you'd rather it use `config.dev.yml`, just set the NODE_ENV environment variable like this: +``` +NODE_ENV=development ng e2e +``` + +The `ng e2e` command will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results. #### Writing E2E Tests From bc705df1444b7d5c8de548b089bd55de06536ab1 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 4 Mar 2022 17:02:42 -0600 Subject: [PATCH 067/435] Check two locations for config.json, as it's in a different location in CI --- cypress/plugins/index.ts | 23 +++++++++++++++++++++-- cypress/support/commands.ts | 6 +++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index c6eb874232..ead38afb92 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -1,15 +1,34 @@ +const fs = require('fs'); + // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress // For more info, visit https://on.cypress.io/plugins-api module.exports = (on, config) => { - // Define "log" and "table" tasks, used for logging accessibility errors during CI - // Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file on('task', { + // Define "log" and "table" tasks, used for logging accessibility errors during CI + // Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file log(message: string) { console.log(message); return null; }, table(message: string) { console.table(message); + return null; + }, + // Cypress doesn't have access to the running application in Node.js. + // So, it's not possible to inject or load the AppConfig or environment of the Angular UI. + // Instead, we'll read our running application's config.json, which contains the configs & + // is regenerated at runtime each time the Angular UI application starts up. + readUIConfig() { + // Check if we have a config.json in the src/assets. If so, use that. + // This is where it's written when running "ng e2e" or "yarn serve" + if (fs.existsSync('./src/assets/config.json')) { + return fs.readFileSync('./src/assets/config.json', 'utf8'); + // Otherwise, check the dist/browser/assets + // This is where it's written when running "serve:ssr", which is what CI uses to start the frontend + } else if (fs.existsSync('./dist/browser/assets/config.json')) { + return fs.readFileSync('./dist/browser/assets/config.json', 'utf8'); + } + return null; } }); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index d66e1ef0bd..30951d46f1 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -34,9 +34,9 @@ function login(email: string, password: string): void { // So, it's not possible to inject or load the AppConfig or environment of the Angular UI. // Instead, we'll read our running application's config.json, which contains the configs & // is regenerated at runtime each time the Angular UI application starts up. - cy.readFile('./src/assets/config.json').then((str) => { - // Parse JSON file into a JSON object - const config = JSON.parse(JSON.stringify(str)); + cy.task('readUIConfig').then((str: string) => { + // Parse config into a JSON object + const config = JSON.parse(str); // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found. let baseRestUrl = FALLBACK_TEST_REST_BASE_URL; From 59f03817b36d51a5be77df0508ec097926a3e9f8 Mon Sep 17 00:00:00 2001 From: YPaulsen-TLC Date: Mon, 20 Dec 2021 12:58:51 +0100 Subject: [PATCH 068/435] DSpace translation German translations of DSpace 7 message keys. --- src/assets/i18n/de.json5 | 100 +++++++++++++-------------------------- 1 file changed, 33 insertions(+), 67 deletions(-) diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index b414852bcd..7edfd27993 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -768,31 +768,28 @@ // "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.", // TODO New key - Add a translation "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.", + // Der erste Tag, ab wann der Zugriff möglich ist. Dieses Datum kann in dieser Form nicht verändert werden. Um eine Embargo-Frist für eine Datei festzulegen, zu dem ()-Tab gehen, auf () klicken, die ()-Richtline für die Datei erstellen oder verändern und das Startdatum wie gewünscht einstellen. // "bitstream.edit.form.embargo.label": "Embargo until specific date", "bitstream.edit.form.embargo.label": "Embargo bis zu einem bestimmten Datum", // "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.", - // TODO New key - Add a translation - "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.hint": "Ändern Sie den Dateinamen für die Datei. Beachten Sie, dass sich dadurch die angezeigte Datei-URL ändert, aber alte Links weiterhin funktionieren, solange sich die Sequenz-ID nicht ändert.", // "bitstream.edit.form.fileName.label": "Filename", "bitstream.edit.form.fileName.label": "Dateiname", // "bitstream.edit.form.newFormat.label": "Describe new format", - // TODO New key - Add a translation - "bitstream.edit.form.newFormat.label": "Describe new format", + "bitstream.edit.form.newFormat.label": "Neues Format beschreiben", // "bitstream.edit.form.newFormat.hint": "The application you used to create the file, and the version number (for example, \"ACMESoft SuperApp version 1.5\").", - // TODO New key - Add a translation - "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.newFormat.hint": "Die Anwendung, mit der Sie die Datei erstellt haben, und die Versionsnummer (zum Beispiel, \"ACMESoft SuperApp version 1.5\").", // "bitstream.edit.form.primaryBitstream.label": "Primary bitstream", "bitstream.edit.form.primaryBitstream.label": "Primäre Datei", // "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\".", - // TODO New key - Add a translation - "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.hint": "Wenn das Format nicht in der obigen Liste enthalten ist, wählen Sie \"Format in der Liste\" und beschreiben Sie es unter \"Neues Format beschreiben\".", // "bitstream.edit.form.selectedFormat.label": "Selected Format", "bitstream.edit.form.selectedFormat.label": "Ausgewähltes Format", @@ -1153,23 +1150,19 @@ // "collection.edit.template.head": "Edit Template Item for Collection \"{{ collection }}\"", // TODO New key - Add a translation - "collection.edit.template.head": "Edit Template Item for Collection \"{{ collection }}\"", + "collection.edit.template.head": "Itemvorlage für Collection \"{{ collection }}\" bearbeiten", // "collection.edit.template.label": "Template item", - // TODO New key - Add a translation - "collection.edit.template.label": "Template item", + "collection.edit.template.label": "Itemvorlage", // "collection.edit.template.notifications.delete.error": "Failed to delete the item template", - // TODO New key - Add a translation - "collection.edit.template.notifications.delete.error": "Failed to delete the item template", + "collection.edit.template.notifications.delete.error": "Löschen der Itemvorlage fehlgeschlagen", // "collection.edit.template.notifications.delete.success": "Successfully deleted the item template", - // TODO New key - Add a translation - "collection.edit.template.notifications.delete.success": "Successfully deleted the item template", + "collection.edit.template.notifications.delete.success": "Die Itemvorlage wurde erfolgreich gelöscht", // "collection.edit.template.title": "Edit Template Item", - // TODO New key - Add a translation - "collection.edit.template.title": "Edit Template Item", + "collection.edit.template.title": "Itemvorlage bearbeiten", @@ -1393,8 +1386,7 @@ // "comcol-role.edit.submitters.name": "Submitters", - // TODO New key - Add a translation - "comcol-role.edit.submitters.name": "Submitters", + "comcol-role.edit.submitters.name": "Einreichende", // "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", // TODO New key - Add a translation @@ -1410,21 +1402,18 @@ "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.", - // TODO New key - Add a translation - "comcol-role.edit.item_read.anonymous-group": "Default read for incoming items is currently set to Anonymous.", + "comcol-role.edit.item_read.anonymous-group": "Die Standardeinstellung für das Lesen eingehender Items ist derzeit auf Anonym eingestellt.", // "comcol-role.edit.bitstream_read.name": "Default bitstream read access", - // TODO New key - Add a translation - "comcol-role.edit.bitstream_read.name": "Default bitstream read access", + "comcol-role.edit.bitstream_read.name": "Standard-Lesezugriff Datei", // "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).", // TODO New key - Add a translation "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.", - // TODO New key - Add a translation - "comcol-role.edit.bitstream_read.anonymous-group": "Default read for incoming bitstreams is currently set to Anonymous.", + "comcol-role.edit.bitstream_read.anonymous-group": "Die Standardeinstellung für das Lesen eingehender Dateien ist derzeit auf Anonym eingestellt.", // "comcol-role.edit.editor.name": "Editors", @@ -1435,21 +1424,17 @@ // "comcol-role.edit.finaleditor.name": "Final editors", - // TODO New key - Add a translation - "comcol-role.edit.finaleditor.name": "Final editors", + "comcol-role.edit.finaleditor.name": "Endredakteure", // "comcol-role.edit.finaleditor.description": "Final editors are able to edit the metadata of incoming submissions, but will not be able to reject them.", - // TODO New key - Add a translation - "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.finaleditor.description": "Die Endredakteure können die Metadaten der eingehenden Beiträge bearbeiten, sie können sie jedoch nicht ablehnen.", // "comcol-role.edit.reviewer.name": "Reviewers", - // TODO New key - Add a translation - "comcol-role.edit.reviewer.name": "Reviewers", + "comcol-role.edit.reviewer.name": "Prüfer", // "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.", - // TODO New key - Add a translation - "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.", + "comcol-role.edit.reviewer.description": "Die Prüfer können eingehende Beiträge annehmen oder ablehnen. Sie können jedoch nicht die Metadaten der Einreichung bearbeiten.", @@ -1509,16 +1494,13 @@ "cookies.consent.app.opt-out.title": "(opt-out)", // "cookies.consent.app.purpose": "purpose", - // TODO New key - Add a translation - "cookies.consent.app.purpose": "purpose", + "cookies.consent.app.purpose": "Verwendungszweck", // "cookies.consent.app.required.description": "This application is always required", - // TODO New key - Add a translation - "cookies.consent.app.required.description": "This application is always required", + "cookies.consent.app.required.description": "Dieser Eintrag ist immer erforderlich.", // "cookies.consent.app.required.title": "(always required)", - // TODO New key - Add a translation - "cookies.consent.app.required.title": "(always required)", + "cookies.consent.app.required.title": "(immer erforderlich)", // "cookies.consent.update": "There were changes since your last visit, please update your consent.", "cookies.consent.update": "Seit Ihrem letzten Besuch haben sich Änderungen ergeben, bitte aktualisieren Sie Ihr Einverständnis.", @@ -1569,33 +1551,27 @@ "cookies.consent.app.title.acknowledgement": "Bestätigung", // "cookies.consent.app.description.acknowledgement": "Required for saving your acknowledgements and consents", - // TODO New key - Add a translation "cookies.consent.app.description.acknowledgement": "Erforderlich für die Speicherung Ihrer Bestätigungen und Einwilligungen", // "cookies.consent.app.title.google-analytics": "Google Analytics", - // TODO New key - Add a translation "cookies.consent.app.title.google-analytics": "Google Analytics", // "cookies.consent.app.description.google-analytics": "Allows us to track statistical data", - // TODO New key - Add a translation - "cookies.consent.app.description.google-analytics": "Allows us to track statistical data", + "cookies.consent.app.description.google-analytics": "Erlaubt uns, statistische Daten zu erfassen", // "cookies.consent.purpose.functional": "Functional", - // TODO New key - Add a translation - "cookies.consent.purpose.functional": "Functional", + "cookies.consent.purpose.functional": "Funktionell", // "cookies.consent.purpose.statistical": "Statistical", - // TODO New key - Add a translation - "cookies.consent.purpose.statistical": "Statistical", + "cookies.consent.purpose.statistical": "Statistisch", // "curation-task.task.checklinks.label": "Check Links in Metadata", - // TODO New key - Add a translation - "curation-task.task.checklinks.label": "Check Links in Metadata", + "curation-task.task.checklinks.label": "Links in Metadaten prüfen", // "curation-task.task.noop.label": "NOOP", // TODO New key - Add a translation @@ -1606,45 +1582,35 @@ "curation-task.task.profileformats.label": "Profile Bitstream Formats", // "curation-task.task.requiredmetadata.label": "Check for Required Metadata", - // TODO New key - Add a translation - "curation-task.task.requiredmetadata.label": "Check for Required Metadata", + "curation-task.task.requiredmetadata.label": "Prüfung auf erforderliche Metadaten", // "curation-task.task.translate.label": "Microsoft Translator", - // TODO New key - Add a translation - "curation-task.task.translate.label": "Microsoft Translator", + "curation-task.task.translate.label": "Microsoft Übersetzer", // "curation-task.task.vscan.label": "Virus Scan", - // TODO New key - Add a translation - "curation-task.task.vscan.label": "Virus Scan", + "curation-task.task.vscan.label": "Viren-Scan", // "curation.form.task-select.label": "Task:", - // TODO New key - Add a translation - "curation.form.task-select.label": "Task:", + "curation.form.task-select.label": "Aufgabe:", // "curation.form.submit": "Start", - // TODO New key - Add a translation "curation.form.submit": "Start", // "curation.form.submit.success.head": "The curation task has been started successfully", - // TODO New key - Add a translation - "curation.form.submit.success.head": "The curation task has been started successfully", + "curation.form.submit.success.head": "Die Datenpflegeroutine wurde erfolgreich gestartet", // "curation.form.submit.success.content": "You will be redirected to the corresponding process page.", - // TODO New key - Add a translation - "curation.form.submit.success.content": "You will be redirected to the corresponding process page.", + "curation.form.submit.success.content": "Sie werden auf die entsprechende Prozessseite weitergeleitet.", // "curation.form.submit.error.head": "Running the curation task failed", - // TODO New key - Add a translation - "curation.form.submit.error.head": "Running the curation task failed", + "curation.form.submit.error.head": "Die Ausführung der Datenpflegeroutine ist fehlgeschlagen", // "curation.form.submit.error.content": "An error occured when trying to start the curation task.", - // TODO New key - Add a translation - "curation.form.submit.error.content": "An error occured when trying to start the curation task.", + "curation.form.submit.error.content": "Beim Versuch, die Datenpflegeroutine zu starten, ist ein Fehler aufgetreten.", // "curation.form.handle.label": "Handle:", - // TODO New key - Add a translation "curation.form.handle.label": "Handle:", // "curation.form.handle.hint": "Hint: Enter [your-handle-prefix]/0 to run a task across entire site (not all tasks may support this capability)", From 8b0c0348b2d582bb106ccc00ab67171017763b74 Mon Sep 17 00:00:00 2001 From: YPaulsen-TLC Date: Fri, 7 Jan 2022 14:34:18 +0100 Subject: [PATCH 069/435] New message key translations German translations of DSpace 7 message keys. --- src/assets/i18n/de.json5 | 400 +++++++++++++++------------------------ 1 file changed, 152 insertions(+), 248 deletions(-) diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index 7edfd27993..77105cc7aa 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -107,7 +107,7 @@ "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 gekennzeichnet 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/der Endnutzer:in verborgen.", // "admin.registries.bitstream-formats.edit.internal.label": "Internal", "admin.registries.bitstream-formats.edit.internal.label": "Intern", @@ -812,7 +812,7 @@ // "browse.comcol.by.author": "By Author", - "browse.comcol.by.author": "Nach Autor/in", + "browse.comcol.by.author": "Nach Autor:in", // "browse.comcol.by.dateissued": "By Issue Date", "browse.comcol.by.dateissued": "Nach Erscheinungsjahr", @@ -830,7 +830,7 @@ "browse.empty": "Es gibt keine Dokumente, die angezeigt werden können.", // "browse.metadata.author": "Author", - "browse.metadata.author": "Autor/in", + "browse.metadata.author": "Autor:in", // "browse.metadata.dateissued": "Issue Date", "browse.metadata.dateissued": "Erscheinungsdatum", @@ -842,7 +842,7 @@ "browse.metadata.title": "Titel", // "browse.metadata.author.breadcrumbs": "Browse by Author", - "browse.metadata.author.breadcrumbs": "Auflistung nach Autor(in)", + "browse.metadata.author.breadcrumbs": "Auflistung nach Autor:in", // "browse.metadata.dateissued.breadcrumbs": "Browse by Date", "browse.metadata.dateissued.breadcrumbs": "Auflistung nach Datum", @@ -978,7 +978,7 @@ "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.description": "Sammlungsadministrator:innen 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", @@ -1370,10 +1370,10 @@ // "comcol-role.edit.community-admin.name": "Administrators", - "comcol-role.edit.community-admin.name": "Administratoren", + "comcol-role.edit.community-admin.name": "Administrator:innen", // "comcol-role.edit.collection-admin.name": "Administrators", - "comcol-role.edit.collection-admin.name": "Administratoren", + "comcol-role.edit.collection-admin.name": "Administrator:innen", // "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).", @@ -1417,24 +1417,24 @@ // "comcol-role.edit.editor.name": "Editors", - "comcol-role.edit.editor.name": "Redakteure", + "comcol-role.edit.editor.name": "Redakteur:innen", // "comcol-role.edit.editor.description": "Editors are able to edit the metadata of incoming submissions, and then accept or reject them.", "comcol-role.edit.editor.description": "Die Redakteure können die Metadaten der eingehenden Beiträge bearbeiten und sie dann annehmen oder ablehnen.", // "comcol-role.edit.finaleditor.name": "Final editors", - "comcol-role.edit.finaleditor.name": "Endredakteure", + "comcol-role.edit.finaleditor.name": "Endredakteur:innen", // "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.finaleditor.description": "Die Endredakteure können die Metadaten der eingehenden Beiträge bearbeiten, sie können sie jedoch nicht ablehnen.", + "comcol-role.edit.finaleditor.description": "Die Endredakteur:innen können die Metadaten der eingehenden Beiträge bearbeiten, sie können sie jedoch nicht ablehnen.", // "comcol-role.edit.reviewer.name": "Reviewers", - "comcol-role.edit.reviewer.name": "Prüfer", + "comcol-role.edit.reviewer.name": "Prüfer:innen", // "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.", - "comcol-role.edit.reviewer.description": "Die Prüfer können eingehende Beiträge annehmen oder ablehnen. Sie können jedoch nicht die Metadaten der Einreichung bearbeiten.", + "comcol-role.edit.reviewer.description": "Die Prüfer:innen können eingehende Beiträge annehmen oder ablehnen. Sie können jedoch nicht die Metadaten der Einreichung bearbeiten.", @@ -1623,8 +1623,7 @@ "dso-selector.create.collection.head": "Neue Sammlung", // "dso-selector.create.collection.sub-level": "Create a new collection in", - // TODO New key - Add a translation - "dso-selector.create.collection.sub-level": "Create a new collection in", + "dso-selector.create.collection.sub-level": "Eine neue Sammlung erstellen in", // "dso-selector.create.community.head": "New community", "dso-selector.create.community.head": "Neuer Bereich", @@ -1639,12 +1638,10 @@ "dso-selector.create.item.head": "Neue Ressource", // "dso-selector.create.item.sub-level": "Create a new item in", - // TODO New key - Add a translation - "dso-selector.create.item.sub-level": "Create a new item in", + "dso-selector.create.item.sub-level": "Ein neues Item erstellen in", // "dso-selector.create.submission.head": "New submission", - // TODO New key - Add a translation - "dso-selector.create.submission.head": "New submission", + "dso-selector.create.submission.head": "Neue Veröffentlichung", // "dso-selector.edit.collection.head": "Edit collection", "dso-selector.edit.collection.head": "Sammlung bearbeiten", @@ -1656,8 +1653,7 @@ "dso-selector.edit.item.head": "Ressource bearbeiten", // "dso-selector.export-metadata.dspaceobject.head": "Export metadata from", - // TODO New key - Add a translation - "dso-selector.export-metadata.dspaceobject.head": "Export metadata from", + "dso-selector.export-metadata.dspaceobject.head": "Exportieren der Metadaten aus", // "dso-selector.no-results": "No {{ type }} found", "dso-selector.no-results": "Kein(e) {{ type }} gefunden", @@ -1668,36 +1664,28 @@ // "confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}", - // TODO New key - Add a translation - "confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}", + "confirmation-modal.export-metadata.header": "Exportieren der Metadaten für {{ dsoName }}", // "confirmation-modal.export-metadata.info": "Are you sure you want to export metadata for {{ dsoName }}", - // TODO New key - Add a translation - "confirmation-modal.export-metadata.info": "Are you sure you want to export metadata for {{ dsoName }}", + "confirmation-modal.export-metadata.info": "Sind Sie sicher, dass Sie die Metadaten für {{ dsoName }} exportieren wollen", // "confirmation-modal.export-metadata.cancel": "Cancel", - // TODO New key - Add a translation - "confirmation-modal.export-metadata.cancel": "Cancel", + "confirmation-modal.export-metadata.cancel": "Abbrechen", // "confirmation-modal.export-metadata.confirm": "Export", - // TODO New key - Add a translation - "confirmation-modal.export-metadata.confirm": "Export", + "confirmation-modal.export-metadata.confirm": "Exportieren", // "confirmation-modal.delete-eperson.header": "Delete EPerson \"{{ dsoName }}\"", - // TODO New key - Add a translation - "confirmation-modal.delete-eperson.header": "Delete EPerson \"{{ dsoName }}\"", + "confirmation-modal.delete-eperson.header": "Person \"{{ dsoName }}\" löschen", // "confirmation-modal.delete-eperson.info": "Are you sure you want to delete EPerson \"{{ dsoName }}\"", - // TODO New key - Add a translation - "confirmation-modal.delete-eperson.info": "Are you sure you want to delete EPerson \"{{ dsoName }}\"", + "confirmation-modal.delete-eperson.info": "Sind Sie sicher, dass Sie die Person \"{{ dsoName }}\" löschen möchten", // "confirmation-modal.delete-eperson.cancel": "Cancel", - // TODO New key - Add a translation - "confirmation-modal.delete-eperson.cancel": "Cancel", + "confirmation-modal.delete-eperson.cancel": "Abbrechen", // "confirmation-modal.delete-eperson.confirm": "Delete", - // TODO New key - Add a translation - "confirmation-modal.delete-eperson.confirm": "Delete", + "confirmation-modal.delete-eperson.confirm": "Löschen", // "error.bitstream": "Error fetching bitstream", @@ -1726,7 +1714,7 @@ "error.item": "Fehler beim Laden der Ressource", // "error.items": "Error fetching items", - "error.items": "Fejöer beim Laden der Ressourcen", + "error.items": "Fehler beim Laden der Ressourcen", // "error.objects": "Error fetching objects", "error.objects": "Fehler beim Laden der Objekte", @@ -1761,8 +1749,7 @@ // "file-section.error.header": "Error obtaining files for this item", - // TODO New key - Add a translation - "file-section.error.header": "Error obtaining files for this item", + "file-section.error.header": "Fehler beim Abrufen der Dateien für dieses Item", @@ -1776,124 +1763,98 @@ "footer.link.lyrasis": "LYRASIS", // "footer.link.cookies": "Cookie settings", - // TODO New key - Add a translation - "footer.link.cookies": "Cookie settings", + "footer.link.cookies": "Cookie Einstellungen", // "footer.link.privacy-policy": "Privacy policy", - // TODO New key - Add a translation - "footer.link.privacy-policy": "Privacy policy", + "footer.link.privacy-policy": "Datenschutzbestimmungen", // "footer.link.end-user-agreement":"End User Agreement", - // TODO New key - Add a translation - "footer.link.end-user-agreement":"End User Agreement", - + "footer.link.end-user-agreement":"Endnutzervereinbarung", + // "forgot-email.form.header": "Forgot Password", - // TODO New key - Add a translation - "forgot-email.form.header": "Forgot Password", + "forgot-email.form.header": "Passwort vergessen", // "forgot-email.form.info": "Enter Register an account to subscribe to collections for email updates, and submit new items to DSpace.", // TODO New key - Add a translation "forgot-email.form.info": "Enter Register an account to subscribe to collections for email updates, and submit new items to DSpace.", // "forgot-email.form.email": "Email Address *", - // TODO New key - Add a translation - "forgot-email.form.email": "Email Address *", + "forgot-email.form.email": "E-Mail-Addresse *", // "forgot-email.form.email.error.required": "Please fill in an email address", - // TODO New key - Add a translation - "forgot-email.form.email.error.required": "Please fill in an email address", + "forgot-email.form.email.error.required": "Bitte geben Sie eine E-Mail-Adresse ein", // "forgot-email.form.email.error.pattern": "Please fill in a valid email address", - // TODO New key - Add a translation - "forgot-email.form.email.error.pattern": "Please fill in a valid email address", + "forgot-email.form.email.error.pattern": "Bitte geben Sie eine gültige E-Mail-Adresse ein", // "forgot-email.form.email.hint": "This address will be verified and used as your login name.", - // TODO New key - Add a translation - "forgot-email.form.email.hint": "This address will be verified and used as your login name.", + "forgot-email.form.email.hint": "Diese Adresse wird überprüft und als Ihr Anmeldename verwendet.", // "forgot-email.form.submit": "Submit", - // TODO New key - Add a translation - "forgot-email.form.submit": "Submit", + "forgot-email.form.submit": "Einreichen", // "forgot-email.form.success.head": "Verification email sent", - // TODO New key - Add a translation - "forgot-email.form.success.head": "Verification email sent", + "forgot-email.form.success.head": "Verifizierungs-E-Mail gesendet", // "forgot-email.form.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", // TODO New key - Add a translation "forgot-email.form.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", // "forgot-email.form.error.head": "Error when trying to register email", - // TODO New key - Add a translation - "forgot-email.form.error.head": "Error when trying to register email", + "forgot-email.form.error.head": "Fehler beim Versuch, die E-Mail zu registrieren", // "forgot-email.form.error.content": "An error occured when registering the following email address: {{ email }}", // TODO New key - Add a translation - "forgot-email.form.error.content": "An error occured when registering the following email address: {{ email }}", + "forgot-email.form.error.content": "Bei der Registrierung der folgenden E-Mail-Adresse ist ein Fehler aufgetreten: {{ email }}", // "forgot-password.title": "Forgot Password", - // TODO New key - Add a translation - "forgot-password.title": "Forgot Password", + "forgot-password.title": "Passwort vergessen", // "forgot-password.form.head": "Forgot Password", - // TODO New key - Add a translation - "forgot-password.form.head": "Forgot Password", + "forgot-password.form.head": "Passwort vergessen", // "forgot-password.form.info": "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.", - // TODO New key - Add a translation - "forgot-password.form.info": "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.", + "forgot-password.form.info": "Geben Sie ein neues Kennwort in das unten stehende Feld ein und bestätigen Sie es, indem Sie es erneut in das zweite Feld eingeben. Es sollte mindestens sechs Zeichen lang sein.", // "forgot-password.form.card.security": "Security", - // TODO New key - Add a translation - "forgot-password.form.card.security": "Security", + "forgot-password.form.card.security": "Sicherheit", // "forgot-password.form.identification.header": "Identify", - // TODO New key - Add a translation - "forgot-password.form.identification.header": "Identify", + "forgot-password.form.identification.header": "Identifizieren", // "forgot-password.form.identification.email": "Email address: ", - // TODO New key - Add a translation - "forgot-password.form.identification.email": "Email address: ", + "forgot-password.form.identification.email": "E-Mail-Addresse: ", // "forgot-password.form.label.password": "Password", - // TODO New key - Add a translation - "forgot-password.form.label.password": "Password", + "forgot-password.form.label.password": "Passwort", // "forgot-password.form.label.passwordrepeat": "Retype to confirm", - // TODO New key - Add a translation - "forgot-password.form.label.passwordrepeat": "Retype to confirm", + "forgot-password.form.label.passwordrepeat": "Zum Bestätigen wiederholen", // "forgot-password.form.error.empty-password": "Please enter a password in the box below.", - // TODO New key - Add a translation - "forgot-password.form.error.empty-password": "Please enter a password in the box below.", + "forgot-password.form.error.empty-password": "Bitte geben Sie in das unten stehende Feld ein Passwort ein.", // "forgot-password.form.error.matching-passwords": "The passwords do not match.", - // TODO New key - Add a translation - "forgot-password.form.error.matching-passwords": "The passwords do not match.", + "forgot-password.form.error.matching-passwords": "Die Passwörter stimmen nicht überein.", // "forgot-password.form.error.password-length": "The password should be at least 6 characters long.", - // TODO New key - Add a translation - "forgot-password.form.error.password-length": "The password should be at least 6 characters long.", + "forgot-password.form.error.password-length": "Das Passwort sollte mindestens 6 Zeichen lang sein.", // "forgot-password.form.notification.error.title": "Error when trying to submit new password", - // TODO New key - Add a translation - "forgot-password.form.notification.error.title": "Error when trying to submit new password", + "forgot-password.form.notification.error.title": "Fehler beim Versuch, ein neues Passwort zu übermitteln", // "forgot-password.form.notification.success.content": "The password reset was successful. You have been logged in as the created user.", - // TODO New key - Add a translation - "forgot-password.form.notification.success.content": "The password reset was successful. You have been logged in as the created user.", + "forgot-password.form.notification.success.content": "Das Zurücksetzen des Passworts war erfolgreich. Sie wurden in den erstellten Account eingeloggt.", // "forgot-password.form.notification.success.title": "Password reset completed", - // TODO New key - Add a translation - "forgot-password.form.notification.success.title": "Password reset completed", + "forgot-password.form.notification.success.title": "Passwort zurücksetzen abgeschlossen", // "forgot-password.form.submit": "Submit password", - // TODO New key - Add a translation - "forgot-password.form.submit": "Submit password", + "forgot-password.form.submit": "Passwort abschicken", @@ -1967,7 +1928,6 @@ "form.search": "Suche", // "form.search-help": "Click here to look for an existing correspondence", - // TODO Source message changed - Revise the translation "form.search-help": "Klicken Sie hier, um eine Übereinstimmung zu suchen", // "form.submit": "Submit", @@ -1979,8 +1939,7 @@ "home.description": "", // "home.breadcrumbs": "Home", - // TODO New key - Add a translation - "home.breadcrumbs": "Home", + "home.breadcrumbs": "Startseite", // "search.search-form.placeholder": "Search the repository ...", "home.search-form.placeholder": "Durchsuche Repositorium", @@ -2032,12 +1991,10 @@ // "item.alerts.private": "This item is private", - // TODO New key - Add a translation - "item.alerts.private": "This item is private", + "item.alerts.private": "Dieses Item ist privat", // "item.alerts.withdrawn": "This item has been withdrawn", - // TODO New key - Add a translation - "item.alerts.withdrawn": "This item has been withdrawn", + "item.alerts.withdrawn": "Dieses Item wurde zurückgezogen", @@ -2046,8 +2003,7 @@ "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", // "item.edit.authorizations.title": "Edit item's Policies", - // TODO New key - Add a translation - "item.edit.authorizations.title": "Edit item's Policies", + "item.edit.authorizations.title": "Item-Richtlinien bearbeiten", @@ -2079,7 +2035,6 @@ "item.bitstreams.upload.drop-message": "Eine Datei zum Hochladen ablegen", // "item.bitstreams.upload.item": "Item: ", - // TODO New key - Add a translation "item.bitstreams.upload.item": "Item: ", // "item.bitstreams.upload.notifications.bundle.created.content": "Successfully created new bundle.", @@ -2092,8 +2047,7 @@ "item.bitstreams.upload.notifications.upload.failed": "Hochladen fehlgeschlagen. Bitte überprüfen Sie den Inhalt, bevor Sie es erneut versuchen.", // "item.bitstreams.upload.title": "Upload bitstream", - // TODO New key - Add a translation - "item.bitstreams.upload.title": "Upload bitstream", + "item.bitstreams.upload.title": "Datei hochladen", @@ -2105,8 +2059,7 @@ "item.edit.bitstreams.bundle.displaying": "Currently displaying {{ amount }} bitstreams of {{ total }}.", // "item.edit.bitstreams.bundle.load.all": "Load all ({{ total }})", - // TODO New key - Add a translation - "item.edit.bitstreams.bundle.load.all": "Load all ({{ total }})", + "item.edit.bitstreams.bundle.load.all": "Alle ({{ total }}) laden", // "item.edit.bitstreams.bundle.load.more": "Load more", "item.edit.bitstreams.bundle.load.more": "Mehr laden", @@ -2168,7 +2121,7 @@ "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.content": "Die Ressource, an der Sie gerade arbeiten, wurde von einem anderen Benutzer geändert. Ihre aktuellen Änderungen werden verworfen, um Konflikte zu vermeiden.", + "item.edit.bitstreams.notifications.outdated.content": "Die Ressource, an der Sie gerade arbeiten, wurde von einem anderen Account geändert. Ihre aktuellen Änderungen werden verworfen, um Konflikte zu vermeiden.", // "item.edit.bitstreams.notifications.outdated.title": "Changes outdated", "item.edit.bitstreams.notifications.outdated.title": "Veraltete Änderungen", @@ -2236,7 +2189,7 @@ "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.description": "Sammlungsadministrator:innen 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 Ressource in andere Sammlungen", @@ -2322,8 +2275,7 @@ "item.edit.metadata.notifications.discarded.title": "Änderungen verworfen", // "item.edit.metadata.notifications.error.title": "An error occurred", - // TODO New key - Add a translation - "item.edit.metadata.notifications.error.title": "An error occurred", + "item.edit.metadata.notifications.error.title": "Es ist ein Fehler aufgetreten", // "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.", @@ -2332,7 +2284,7 @@ "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.content": "Die Ressource, an der Sie gerade arbeiten, wurde von einem anderen Account 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", @@ -2482,7 +2434,7 @@ "item.edit.relationships.notifications.failed.title": "Fehler beim Bearbeiten 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.content": "Die Ressource, die Sie gerade bearbeiten, wurde von einem anderen Account 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", @@ -2530,7 +2482,7 @@ "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.button": "Berechtigungen...", // "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", "item.edit.tabs.status.buttons.authorizations.label": "Rechte der Ressource bearbeiten", @@ -2671,7 +2623,7 @@ "item.page.abstract": "Zusammenfassung", // "item.page.author": "Authors", - "item.page.author": "AutorInnen", + "item.page.author": "Autor:innen", // "item.page.citation": "Citation", "item.page.citation": "Zitierform", @@ -2713,7 +2665,7 @@ "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.person.search.title": "Veröffentlichungen dieses/dieser Autor:in", // "item.page.related-items.view-more": "Show {{ amount }} more", "item.page.related-items.view-more": "{{ amount }} mehr anzeigen", @@ -2728,7 +2680,7 @@ "item.page.relationships.isJournalOfPublication": "Veröffentlichungen", // "item.page.relationships.isOrgUnitOfPerson": "Authors", - "item.page.relationships.isOrgUnitOfPerson": "AutorInnen", + "item.page.relationships.isOrgUnitOfPerson": "Autor:innen", // "item.page.relationships.isOrgUnitOfProject": "Research Projects", "item.page.relationships.isOrgUnitOfProject": "Forschungsprojekte", @@ -2755,7 +2707,7 @@ "item.preview.dc.identifier.uri": "Identifikator:", // "item.preview.dc.contributor.author": "Authors:", - "item.preview.dc.contributor.author": "AutorInnen:", + "item.preview.dc.contributor.author": "Autor:innen:", // "item.preview.dc.date.issued": "Published date:", "item.preview.dc.date.issued": "Erscheinungsdatum:", @@ -2793,7 +2745,7 @@ "item.select.empty": "Es gibt keine Ressourcen dazu", // "item.select.table.author": "Author", - "item.select.table.author": "Autor/in", + "item.select.table.author": "Autor:in", // "item.select.table.collection": "Collection", "item.select.table.collection": "Sammlung", @@ -2809,7 +2761,6 @@ "item.version.history.head": "Versionsgeschichte", // "item.version.history.return": "Return", - // TODO New key - Add a translation "item.version.history.return": "Zurück", // "item.version.history.selected": "Selected version", @@ -2822,7 +2773,7 @@ "item.version.history.table.item": "Ressource", // "item.version.history.table.editor": "Editor", - "item.version.history.table.editor": "Herausgeber/in", + "item.version.history.table.editor": "Herausgeber:in", // "item.version.history.table.date": "Date", "item.version.history.table.date": "Datum", @@ -2848,7 +2799,7 @@ "journal.page.edit": "Diese Ressource bearbeiten", // "journal.page.editor": "Editor-in-Chief", - "journal.page.editor": "Chefredakteur", + "journal.page.editor": "Chefredakteur:in", // "journal.page.issn": "ISSN", "journal.page.issn": "ISSN", @@ -3038,7 +2989,7 @@ // "menu.section.admin_search": "Admin Search", - "menu.section.admin_search": "Administratoren Suche", + "menu.section.admin_search": "Administrator:innen Suche", @@ -3046,7 +2997,7 @@ "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_author": "Nach Autor:in", // "menu.section.browse_community_by_issue_date": "By Issue Date", "menu.section.browse_community_by_issue_date": "Nach Erscheinungsdateum", @@ -3058,7 +3009,7 @@ "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_author": "Nach Autor:in", // "menu.section.browse_global_by_dateissued": "By Issue Date", "menu.section.browse_global_by_dateissued": "Nach Erscheinungsjahr", @@ -3260,8 +3211,7 @@ "mydspace.description": "", // "mydspace.general.text-here": "here", - // TODO New key - Add a translation - "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.", @@ -3318,7 +3268,7 @@ "mydspace.results.no-abstract": "Keine Zusammenfassung", // "mydspace.results.no-authors": "No Authors", - "mydspace.results.no-authors": "Keine AutorInnen", + "mydspace.results.no-authors": "Keine Autor:innen", // "mydspace.results.no-collections": "No Collections", "mydspace.results.no-collections": "Keine Sammlungen", @@ -3587,14 +3537,12 @@ "process.detail.output-files" : "Output Files", // "process.detail.output-files.empty" : "This process doesn't contain any output files", - // TODO New key - Add a translation - "process.detail.output-files.empty" : "This process doesn't contain any output files", + "process.detail.output-files.empty" : "Dieser Prozess enthält keine Ausgabedateien", // "process.detail.script" : "Script", "process.detail.script" : "Skript", // "process.detail.title" : "Process: {{ id }} - {{ name }}", - // TODO New key - Add a translation "process.detail.title" : "Prozess: {{ id }} - {{ name }}", // "process.detail.start-time" : "Start time", @@ -3627,7 +3575,7 @@ "process.overview.table.status" : "Status", // "process.overview.table.user" : "User", - "process.overview.table.user" : "Benutzer", + "process.overview.table.user" : "Benutzer:in", // "process.overview.title": "Processes Overview", "process.overview.title": "Überblick der Prozesse", @@ -3652,8 +3600,7 @@ "profile.form.submit": "Profil aktualisieren", // "profile.groups.head": "Authorization groups you belong to", - // TODO New key - Add a translation - "profile.groups.head": "Authorization groups you belong to", + "profile.groups.head": "Berechtigungsgruppen, denen Sie angehören", // "profile.head": "Update Profile", "profile.head": "Profil aktualisieren", @@ -3793,7 +3740,7 @@ // "register-email.title": "New user registration", - "register-email.title": "Registrierung neuer Benutzer", + "register-email.title": "Registrierung neuer Benutzer:innen", // "register-page.create-profile.header": "Create Profile", "register-page.create-profile.header": "Profil erstellen", @@ -3847,20 +3794,20 @@ "register-page.create-profile.submit": "Vollständige Registrierung", // "register-page.create-profile.submit.error.content": "Something went wrong while registering a new user.", - "register-page.create-profile.submit.error.content": "Bei der Registrierung eines neuen Benutzers ist ein Fehler aufgetreten.", + "register-page.create-profile.submit.error.content": "Bei der Registrierung eines neuen Accounts ist ein Fehler aufgetreten.", // "register-page.create-profile.submit.error.head": "Registration failed", "register-page.create-profile.submit.error.head": "Registrierung fehlgeschlagen.", // "register-page.create-profile.submit.success.content": "The registration was successful. You have been logged in as the created user.", - "register-page.create-profile.submit.success.content": "Die Registrierung war erfolgreich. Sie wurden als der angelegte Benutzer angemeldet.", + "register-page.create-profile.submit.success.content": "Die Registrierung war erfolgreich. Sie wurden als der/die angelegte Benutzer:in angemeldet.", // "register-page.create-profile.submit.success.head": "Registration completed", "register-page.create-profile.submit.success.head": "Registrierung abgeschlossen", // "register-page.registration.header": "New user registration", - "register-page.registration.header": "Registrierung neuer Benutzer", + "register-page.registration.header": "Registrierung neuer Benutzer:innen", // "register-page.registration.info": "Register an account to subscribe to collections for email updates, and submit new items to DSpace.", "register-page.registration.info": "Registrieren Sie ein Konto, um Sammlungen für E-Mail-Updates zu abonnieren und neue Ressourcen in DSpace einzugeben.", @@ -3875,7 +3822,7 @@ "register-page.registration.email.error.pattern": "Bitte geben Sie eine gültige E-Mail Adresse an", // "register-page.registration.email.hint": "This address will be verified and used as your login name.", - "register-page.registration.email.hint": "Diese Adresse wird überprüft und als Ihr Benutzername verwendet..", + "register-page.registration.email.hint": "Diese Adresse wird überprüft und als Ihr Accountname verwendet..", // "register-page.registration.submit": "Register", "register-page.registration.submit": "Registrieren", @@ -3904,13 +3851,13 @@ "relationships.add.error.title": "Beziehung kann nicht hinzugefügt werden", // "relationships.isAuthorOf": "Authors", - "relationships.isAuthorOf": "AutorInnen", + "relationships.isAuthorOf": "Autor:innen", // "relationships.isAuthorOf.Person": "Authors (persons)", "relationships.isAuthorOf.Person": "AutorInnen (Personen)", // "relationships.isAuthorOf.OrgUnit": "Authors (organizational units)", - "relationships.isAuthorOf.OrgUnit": "AutorInnen (Organisationseinheiten)", + "relationships.isAuthorOf.OrgUnit": "Autor:innen (Organisationseinheiten)", // "relationships.isIssueOf": "Journal Issues", "relationships.isIssueOf": "Zeitschriftenausgaben", @@ -3925,7 +3872,7 @@ "relationships.isOrgUnitOf": "Organisationseinheiten", // "relationships.isPersonOf": "Authors", - "relationships.isPersonOf": "AutorInnen", + "relationships.isPersonOf": "Autor:innen", // "relationships.isProjectOf": "Research Projects", "relationships.isProjectOf": "Forschungsvorhaben", @@ -3957,24 +3904,19 @@ "resource-policies.add.for.": "Eine neue Richtlinie hinzufügen", // "resource-policies.add.for.bitstream": "Add a new Bitstream policy", - // TODO New key - Add a translation - "resource-policies.add.for.bitstream": "Add a new Bitstream policy", + "resource-policies.add.for.bitstream": "Eine neue Datei-Richtlinie hinzufügen", // "resource-policies.add.for.bundle": "Add a new Bundle policy", - // TODO New key - Add a translation - "resource-policies.add.for.bundle": "Add a new Bundle policy", + "resource-policies.add.for.bundle": "Eine neue Bündel-Richtlinie hinzufügen", // "resource-policies.add.for.item": "Add a new Item policy", - // TODO New key - Add a translation - "resource-policies.add.for.item": "Add a new Item policy", + "resource-policies.add.for.item": "Eine neue Item-Richtlinie hinzufügen", // "resource-policies.add.for.community": "Add a new Community policy", - // TODO New key - Add a translation - "resource-policies.add.for.community": "Add a new Community policy", + "resource-policies.add.for.community": "Eine neue Bereiche-Richtlinie hinzufügen", // "resource-policies.add.for.collection": "Add a new Collection policy", - // TODO New key - Add a translation - "resource-policies.add.for.collection": "Add a new Collection policy", + "resource-policies.add.for.collection": "Eine neue Sammlungen-Richtlinie hinzufügen", // "resource-policies.create.page.heading": "Create new resource policy for ", // TODO New key - Add a translation @@ -4021,15 +3963,13 @@ "resource-policies.edit.page.title": "Edit resource policy", // "resource-policies.form.action-type.label": "Select the action type", - // TODO New key - Add a translation - "resource-policies.form.action-type.label": "Select the action type", + "resource-policies.form.action-type.label": "Wählen Sie die Aktionsart", // "resource-policies.form.action-type.required": "You must select the resource policy action.", // TODO New key - Add a translation "resource-policies.form.action-type.required": "You must select the resource policy action.", // "resource-policies.form.eperson-group-list.label": "The eperson or group that will be granted the permission", - // TODO New key - Add a translation "resource-policies.form.eperson-group-list.label": "Die Person oder Gruppe, der die Genehmigung erteilt wird", // "resource-policies.form.eperson-group-list.select.btn": "Select", @@ -4064,16 +4004,14 @@ "resource-policies.form.name.label": "Name", // "resource-policies.form.policy-type.label": "Select the policy type", - // TODO New key - Add a translation - "resource-policies.form.policy-type.label": "Select the policy type", + "resource-policies.form.policy-type.label": "Wählen Sie den Richtlinientyp", // "resource-policies.form.policy-type.required": "You must select the resource policy type.", // TODO New key - Add a translation "resource-policies.form.policy-type.required": "You must select the resource policy type.", // "resource-policies.table.headers.action": "Action", - // TODO New key - Add a translation - "resource-policies.table.headers.action": "Action", + "resource-policies.table.headers.action": "Aktion", // "resource-policies.table.headers.date.end": "End Date", "resource-policies.table.headers.date.end": "Enddatum", @@ -4106,24 +4044,19 @@ "resource-policies.table.headers.policyType": "Typ", // "resource-policies.table.headers.title.for.bitstream": "Policies for Bitstream", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.bitstream": "Policies for Bitstream", + "resource-policies.table.headers.title.for.bitstream": "Richtlinien für Dateien", // "resource-policies.table.headers.title.for.bundle": "Policies for Bundle", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.bundle": "Policies for Bundle", + "resource-policies.table.headers.title.for.bundle": "Richtlinien für Bündel", // "resource-policies.table.headers.title.for.item": "Policies for Item", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.item": "Policies for Item", + "resource-policies.table.headers.title.for.item": "Richtlinien für Items", // "resource-policies.table.headers.title.for.community": "Policies for Community", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.community": "Policies for Community", + "resource-policies.table.headers.title.for.community": "Richtlinien für Bereiche", // "resource-policies.table.headers.title.for.collection": "Policies for Collection", - // TODO New key - Add a translation - "resource-policies.table.headers.title.for.collection": "Policies for Collection", + "resource-policies.table.headers.title.for.collection": "Richtlinien für Sammlungen", @@ -4143,7 +4076,7 @@ "search.search-form.placeholder": "Durchsuche Repositorium", // "search.filters.applied.f.author": "Author", - "search.filters.applied.f.author": "AutorIn", + "search.filters.applied.f.author": "Autor:in", // "search.filters.applied.f.dateIssued.max": "End date", "search.filters.applied.f.dateIssued.max": "Enddatum", @@ -4173,7 +4106,7 @@ "search.filters.applied.f.subject": "Thema", // "search.filters.applied.f.submitter": "Submitter", - "search.filters.applied.f.submitter": "Einreichende(r)", + "search.filters.applied.f.submitter": "Einreichende:r", // "search.filters.applied.f.jobTitle": "Job Title", "search.filters.applied.f.jobTitle": "Berufsbezeichnung", @@ -4190,10 +4123,10 @@ // "search.filters.filter.author.head": "Author", - "search.filters.filter.author.head": "AutorIn", + "search.filters.filter.author.head": "Autor:in", // "search.filters.filter.author.placeholder": "Author name", - "search.filters.filter.author.placeholder": "Name des/r AutorIn", + "search.filters.filter.author.placeholder": "Name des/der Autor:in", // "search.filters.filter.birthDate.head": "Birth Date", "search.filters.filter.birthDate.head": "Geburtsdatum", @@ -4208,10 +4141,10 @@ "search.filters.filter.creativeDatePublished.placeholder": "Erscheinungsdatum", // "search.filters.filter.creativeWorkEditor.head": "Editor", - "search.filters.filter.creativeWorkEditor.head": "Herausgeber", + "search.filters.filter.creativeWorkEditor.head": "Herausgeber:in", // "search.filters.filter.creativeWorkEditor.placeholder": "Editor", - "search.filters.filter.creativeWorkEditor.placeholder": "Herausgeber", + "search.filters.filter.creativeWorkEditor.placeholder": "Herausgeber:in", // "search.filters.filter.creativeWorkKeywords.head": "Subject", "search.filters.filter.creativeWorkKeywords.head": "Thema", @@ -4322,10 +4255,10 @@ "search.filters.filter.subject.placeholder": "Thema", // "search.filters.filter.submitter.head": "Submitter", - "search.filters.filter.submitter.head": "Einreichende(r)", + "search.filters.filter.submitter.head": "Einreichende:r", // "search.filters.filter.submitter.placeholder": "Submitter", - "search.filters.filter.submitter.placeholder": "Einreichende(r)", + "search.filters.filter.submitter.placeholder": "Einreichende:r", @@ -4591,10 +4524,10 @@ "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.title": "Import Remote Author", // "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Successfully added local author to the selection", - "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Erfolgreicher Import und Hinzufügen eines lokalen Autors zur Auswahl", + "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Erfolgreicher Import und Hinzufügen eines/einer lokalen Autor:in zur Auswahl", // "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.new-entity": "Successfully imported and added external author to the selection", - "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.new-entity": "Erfolgreicher Import und Hinzufügen eines externen Autors zur Auswahl", + "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.new-entity": "Erfolgreicher Import und Hinzufügen eines/einer externen Autor:in zur Auswahl", // "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Authority", "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Referenz", @@ -4756,12 +4689,10 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.arxiv": "arXiv ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingAgencyOfPublication": "Search for Funding Agencies", - // TODO New key - Add a translation "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingAgencyOfPublication": "Suche nach Fördereinrichtungen", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingOfPublication": "Search for Funding", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingOfPublication": "Search for Funding", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingOfPublication": "Suche nach Förderungen", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isChildOrgUnitOf": "Search for Organizational Units", "submission.sections.describe.relationship-lookup.search-tab.tab-title.isChildOrgUnitOf": "Suche nach Organisationseinheiten", @@ -4783,7 +4714,7 @@ "submission.sections.describe.relationship-lookup.title.isJournalOfPublication": "Zeitschriften", // "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "Authors", - "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "AutorInnen", + "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "Autor:innen", // "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfPublication": "Funding Agency", "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfPublication": "Fördereinrichtung", @@ -4794,14 +4725,13 @@ "submission.sections.describe.relationship-lookup.title.Publication": "Publikationen", // "submission.sections.describe.relationship-lookup.title.Person": "Authors", - "submission.sections.describe.relationship-lookup.title.Person": "AutorInnen", + "submission.sections.describe.relationship-lookup.title.Person": "Autor:innen", // "submission.sections.describe.relationship-lookup.title.OrgUnit": "Organizational Units", "submission.sections.describe.relationship-lookup.title.OrgUnit": "Organisationseinheiten", // "submission.sections.describe.relationship-lookup.title.DataPackage": "Data Packages", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.DataPackage": "Data Packages", + "submission.sections.describe.relationship-lookup.title.DataPackage": "Datenpakete", // "submission.sections.describe.relationship-lookup.title.DataFile": "Data Files", // TODO New key - Add a translation @@ -4811,12 +4741,10 @@ "submission.sections.describe.relationship-lookup.title.Funding Agency": "Fördereinrichtung", // "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Funding", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Funding", + "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Förderung", // "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Parent Organizational Unit", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Parent Organizational Unit", + "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Übergeordnete Organisationeinheit", // "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown", "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Liste umschalten", @@ -4828,13 +4756,14 @@ "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Ihre Auswahl ist momentan leer.", // "submission.sections.describe.relationship-lookup.selection-tab.title.isAuthorOfPublication": "Selected Authors", - "submission.sections.describe.relationship-lookup.selection-tab.title.isAuthorOfPublication": "Ausgewählte AutorInnen", + "submission.sections.describe.relationship-lookup.selection-tab.title.isAuthorOfPublication": "Ausgewählte Autor:innen", // "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalOfPublication": "Selected Journals", "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalOfPublication": "Ausgewählte Zeitschriften", // "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfPublication": "Selected Journal Volume", "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfPublication": "Ausgewählte Zeitschriftenbände", + // "submission.sections.describe.relationship-lookup.selection-tab.title.Project": "Selected Projects", "submission.sections.describe.relationship-lookup.selection-tab.title.Project": "Ausgewählte Projekte", @@ -4842,14 +4771,13 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.Publication": "Ausgewählte Publikationen", // "submission.sections.describe.relationship-lookup.selection-tab.title.Person": "Selected Authors", - "submission.sections.describe.relationship-lookup.selection-tab.title.Person": "Ausgewählte AutorInnen", + "submission.sections.describe.relationship-lookup.selection-tab.title.Person": "Ausgewählte Autor:innen", // "submission.sections.describe.relationship-lookup.selection-tab.title.OrgUnit": "Selected Organizational Units", "submission.sections.describe.relationship-lookup.selection-tab.title.OrgUnit": "Ausgewählte Organisationseinheiten", // "submission.sections.describe.relationship-lookup.selection-tab.title.DataPackage": "Selected Data Packages", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.DataPackage": "Selected Data Packages", + "submission.sections.describe.relationship-lookup.selection-tab.title.DataPackage": "Ausgewählte Datenpakete", // "submission.sections.describe.relationship-lookup.selection-tab.title.DataFile": "Selected Data Files", // TODO New key - Add a translation @@ -4860,6 +4788,7 @@ // "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalIssueOfPublication": "Selected Issue", "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalIssueOfPublication": "Ausgewählte Ausgaben", + // "submission.sections.describe.relationship-lookup.selection-tab.title.JournalVolume": "Selected Journal Volume", "submission.sections.describe.relationship-lookup.selection-tab.title.JournalVolume": "Ausgewählte Zeitschriftenbände", @@ -4867,8 +4796,8 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingAgencyOfPublication": "Ausgewählte Fördereinrichtung", // "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingOfPublication": "Selected Funding", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingOfPublication": "Selected Funding", + "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingOfPublication": "Ausgewählte Förderung", + // "submission.sections.describe.relationship-lookup.selection-tab.title.JournalIssue": "Selected Issue", "submission.sections.describe.relationship-lookup.selection-tab.title.JournalIssue": "Ausgewählte Ausgabe", @@ -5110,7 +5039,7 @@ "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.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 Einreichende:n 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", @@ -5142,7 +5071,7 @@ "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.submitter": "Einreichende:r", // "submission.workflow.tasks.generic.success": "Operation successful", "submission.workflow.tasks.generic.success": "Aktion erfolgreich", @@ -5169,32 +5098,25 @@ // "vocabulary-treeview.header": "Hierarchical tree view", - // TODO New key - Add a translation - "vocabulary-treeview.header": "Hierarchical tree view", + "vocabulary-treeview.header": "Hierarchische Baumstruktur", // "vocabulary-treeview.load-more": "Load more", - // TODO New key - Add a translation - "vocabulary-treeview.load-more": "Load more", + "vocabulary-treeview.load-more": "Mehr laden", // "vocabulary-treeview.search.form.reset": "Reset", - // TODO New key - Add a translation - "vocabulary-treeview.search.form.reset": "Reset", + "vocabulary-treeview.search.form.reset": "Zurücksetzen", // "vocabulary-treeview.search.form.search": "Search", - // TODO New key - Add a translation - "vocabulary-treeview.search.form.search": "Search", + "vocabulary-treeview.search.form.search": "Suche", // "vocabulary-treeview.search.no-result": "There were no items to show", - // TODO New key - Add a translation - "vocabulary-treeview.search.no-result": "There were no items to show", + "vocabulary-treeview.search.no-result": "Es gibt keine Items zu zeigen", // "vocabulary-treeview.tree.description.nsi": "The Norwegian Science Index", - // TODO New key - Add a translation - "vocabulary-treeview.tree.description.nsi": "The Norwegian Science Index", + "vocabulary-treeview.tree.description.nsi": "Der norwegische Wissenschaftsindex", // "vocabulary-treeview.tree.description.srsc": "Research Subject Categories", - // TODO New key - Add a translation - "vocabulary-treeview.tree.description.srsc": "Research Subject Categories", + "vocabulary-treeview.tree.description.srsc": "Kategorien von Forschungsthemen", @@ -5205,8 +5127,7 @@ "uploader.drag-message": "Ziehen Sie Ihre Dateien hierhin", // "uploader.or": ", or ", - // TODO Source message changed - Revise the translation - "uploader.or": " oder", + "uploader.or": ", oder", // "uploader.processing": "Processing", "uploader.processing": "Bearbeitung läuft", @@ -5226,74 +5147,57 @@ // "workflowAdmin.search.results.head": "Administer Workflow", - // TODO New key - Add a translation - "workflowAdmin.search.results.head": "Administer Workflow", + "workflowAdmin.search.results.head": "Workflow verwalten", // "workflow-item.delete.notification.success.title": "Deleted", - // TODO New key - Add a translation - "workflow-item.delete.notification.success.title": "Deleted", + "workflow-item.delete.notification.success.title": "Gelöscht", // "workflow-item.delete.notification.success.content": "This workflow item was successfully deleted", - // TODO New key - Add a translation - "workflow-item.delete.notification.success.content": "This workflow item was successfully deleted", + "workflow-item.delete.notification.success.content": "Dieses Workflow-Item wurde erfolgreich gelöscht", // "workflow-item.delete.notification.error.title": "Something went wrong", - // TODO New key - Add a translation - "workflow-item.delete.notification.error.title": "Something went wrong", + "workflow-item.delete.notification.error.title": "Etwas ist schief gelaufen", // "workflow-item.delete.notification.error.content": "The workflow item could not be deleted", - // TODO New key - Add a translation - "workflow-item.delete.notification.error.content": "The workflow item could not be deleted", + "workflow-item.delete.notification.error.content": "Das Workflow-Item konnte nicht gelöscht werden", // "workflow-item.delete.title": "Delete workflow item", - // TODO New key - Add a translation - "workflow-item.delete.title": "Delete workflow item", + "workflow-item.delete.title": "Workflow-Item löschen", // "workflow-item.delete.header": "Delete workflow item", - // TODO New key - Add a translation - "workflow-item.delete.header": "Delete workflow item", + "workflow-item.delete.header": "Workflow-Item löschen", // "workflow-item.delete.button.cancel": "Cancel", - // TODO New key - Add a translation - "workflow-item.delete.button.cancel": "Cancel", + "workflow-item.delete.button.cancel": "Abbrechen", // "workflow-item.delete.button.confirm": "Delete", - // TODO New key - Add a translation - "workflow-item.delete.button.confirm": "Delete", + "workflow-item.delete.button.confirm": "Löschen", // "workflow-item.send-back.notification.success.title": "Sent back to submitter", - // TODO New key - Add a translation - "workflow-item.send-back.notification.success.title": "Sent back to submitter", + "workflow-item.send-back.notification.success.title": "An die/den Einreichende:n zurückgeschickt", // "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", - // TODO New key - Add a translation - "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", + "workflow-item.send-back.notification.success.content": "Dieses Workflow-Item wurde erfolgreich an die/den Einreichende:n zurückgeschickt", // "workflow-item.send-back.notification.error.title": "Something went wrong", - // TODO New key - Add a translation - "workflow-item.send-back.notification.error.title": "Something went wrong", + "workflow-item.send-back.notification.error.title": "Etwas ist schief gelaufen", // "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", - // TODO New key - Add a translation - "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", + "workflow-item.send-back.notification.error.content": "Das Workflow-Item konnte nicht an die/den Einreichende:n zurückgeschickt werden", // "workflow-item.send-back.title": "Send workflow item back to submitter", - // TODO New key - Add a translation - "workflow-item.send-back.title": "Send workflow item back to submitter", + "workflow-item.send-back.title": "Workflow-Item an die/den Einreichende:n zurücksenden", // "workflow-item.send-back.header": "Send workflow item back to submitter", - // TODO New key - Add a translation - "workflow-item.send-back.header": "Send workflow item back to submitter", + "workflow-item.send-back.header": "Workflow-Item an die/den Einreichende:n zurücksenden", // "workflow-item.send-back.button.cancel": "Cancel", - // TODO New key - Add a translation - "workflow-item.send-back.button.cancel": "Cancel", + "workflow-item.send-back.button.cancel": "Abbrechen", // "workflow-item.send-back.button.confirm": "Send back" - // TODO New key - Add a translation - "workflow-item.send-back.button.confirm": "Send back" + "workflow-item.send-back.button.confirm": "Zurücksenden" } From 3bbd2d9d6405b802b9a020533c6c9dc9e37ecaa5 Mon Sep 17 00:00:00 2001 From: YPaulsen-TLC Date: Fri, 14 Jan 2022 14:05:41 +0100 Subject: [PATCH 070/435] message key translations Final German message keys translations for DSpace 7, --- src/assets/i18n/de.json5 | 454 +++++++++++++++++---------------------- 1 file changed, 194 insertions(+), 260 deletions(-) diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index 77105cc7aa..d3fc1131aa 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -483,8 +483,7 @@ "admin.access-control.groups.form.alert.permanent": "Diese Gruppe ist permanent, das heißt sie kann nicht bearbeitet oder gelöscht werden. Sie können jedoch über diese Seite Gruppenmitglieder hinzufügen und entfernen.", // "admin.access-control.groups.form.alert.workflowGroup": "This group can’t be modified or deleted because it corresponds to a role in the submission and workflow process in the \"{{name}}\" {{comcol}}. You can delete it from the \"assign roles\" tab on the edit {{comcol}} page. You can still add and remove group members using this page.", - // TODO New key - Add a translation - "admin.access-control.groups.form.alert.workflowGroup": "This group can’t be modified or deleted because it corresponds to a role in the submission and workflow process in the \"{{name}}\" {{comcol}}. You can delete it from the \"assign roles\" tab on the edit {{comcol}} page. You can still add and remove group members using this page.", + "admin.access-control.groups.form.alert.workflowGroup": "Diese Gruppe kann nicht geändert oder gelöscht werden, da sie einer Rolle im Einreichungs- und Workflow-Prozess im \"{{name}}\" {{comcol}} zugeordnet ist. Sie können sie über die Registerkarte \"assign roles\" auf der Bearbeitungsseite {{comcol}} löschen. Sie können über diese Seite weiterhin Gruppenmitglieder hinzufügen und entfernen.", // "admin.access-control.groups.form.head.create": "Create group", "admin.access-control.groups.form.head.create": "Gruppe erstellen", @@ -766,9 +765,8 @@ "bitstream.edit.form.description.label": "Beschreibung", // "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.", - // TODO New key - Add a translation - "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.", - // Der erste Tag, ab wann der Zugriff möglich ist. Dieses Datum kann in dieser Form nicht verändert werden. Um eine Embargo-Frist für eine Datei festzulegen, zu dem ()-Tab gehen, auf () klicken, die ()-Richtline für die Datei erstellen oder verändern und das Startdatum wie gewünscht einstellen. + "bitstream.edit.form.embargo.hint": "Der erste Tag, ab dem der Zugang erlaubt ist. This date cannot be modified on this form. Um eine Embargo-Frist für eine Datei festzulegen, zu Item Status gehen, auf Authorizations... klicken, die Richtlinie READ für die Datei erstellen oder verändern und das Start Date wie gewünscht einstellen.", + // "bitstream.edit.form.embargo.label": "Embargo until specific date", "bitstream.edit.form.embargo.label": "Embargo bis zu einem bestimmten Datum", @@ -827,7 +825,7 @@ "browse.comcol.head": "Listen", // "browse.empty": "No items to show.", - "browse.empty": "Es gibt keine Dokumente, die angezeigt werden können.", + "browse.empty": "Es gibt keine Items, die angezeigt werden können.", // "browse.metadata.author": "Author", "browse.metadata.author": "Autor:in", @@ -963,10 +961,10 @@ // "collection.edit.tabs.mapper.head": "Item Mapper", - "collection.edit.tabs.mapper.head": "Ressource spiegeln", + "collection.edit.tabs.mapper.head": "Item spiegeln", // "collection.edit.tabs.item-mapper.title": "Collection Edit - Item Mapper", - "collection.edit.tabs.item-mapper.title": "Sammlung bearbeiten - Ressource spiegeln", + "collection.edit.tabs.item-mapper.title": "Sammlung bearbeiten - Item spiegeln", // "collection.edit.item-mapper.cancel": "Cancel", "collection.edit.item-mapper.cancel": "Abbrechen", @@ -975,13 +973,13 @@ "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.confirm": "Ausgewählte Items 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": "Sammlungsadministrator:innen 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.description": "Sammlungsadministrator:innen haben die Möglichkeit Items von einer Sammlung in eine andere zu spiegeln. Man kann nach Items in anderen Sammlungen suchen und diese spiegeln oder sich eine Liste der gespiegelten Items 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.head": "Item spiegeln - Spiegelt Items 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.", @@ -993,13 +991,13 @@ "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.content": "{{amount}} Item(s) 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.content": "Beim Spiegeln von {{amount}} Item(s) 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", @@ -1011,13 +1009,13 @@ "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.remove": "Spiegelung der gewählten Items entfernen.", // "collection.edit.item-mapper.tabs.browse": "Browse mapped items", - "collection.edit.item-mapper.tabs.browse": "Gespiegelte Ressourcen auflisten", + "collection.edit.item-mapper.tabs.browse": "Gespiegelte Items auflisten", // "collection.edit.item-mapper.tabs.map": "Map new items", - "collection.edit.item-mapper.tabs.map": "Neue Ressourcen spiegeln", + "collection.edit.item-mapper.tabs.map": "Neue Items spiegeln", @@ -1062,8 +1060,7 @@ "collection.edit.tabs.authorizations.head": "Berechtigungen", // "collection.edit.tabs.authorizations.title": "Collection Edit - Authorizations", - // TODO New key - Add a translation - "collection.edit.tabs.authorizations.title": "Collection Edit - Authorizations", + "collection.edit.tabs.authorizations.title": "Sammlung bearbeiten - Berechtigungen", // "collection.edit.tabs.metadata.head": "Edit Metadata", "collection.edit.tabs.metadata.head": "Metadaten bearbeiten", @@ -1149,20 +1146,19 @@ "collection.edit.template.edit-button": "Bearbeiten", // "collection.edit.template.head": "Edit Template Item for Collection \"{{ collection }}\"", - // TODO New key - Add a translation - "collection.edit.template.head": "Itemvorlage für Collection \"{{ collection }}\" bearbeiten", + "collection.edit.template.head": "Itemvorlage für Sammlung \"{{ collection }}\" bearbeiten", // "collection.edit.template.label": "Template item", "collection.edit.template.label": "Itemvorlage", // "collection.edit.template.notifications.delete.error": "Failed to delete the item template", - "collection.edit.template.notifications.delete.error": "Löschen der Itemvorlage fehlgeschlagen", + "collection.edit.template.notifications.delete.error": "Löschen der Metadatenvorlage fehlgeschlagen", // "collection.edit.template.notifications.delete.success": "Successfully deleted the item template", - "collection.edit.template.notifications.delete.success": "Die Itemvorlage wurde erfolgreich gelöscht", + "collection.edit.template.notifications.delete.success": "Die Metadatenvorlage wurde erfolgreich gelöscht", // "collection.edit.template.title": "Edit Template Item", - "collection.edit.template.title": "Itemvorlage bearbeiten", + "collection.edit.template.title": "Metadatenvorlage bearbeiten", @@ -1201,7 +1197,7 @@ "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.browse.recent.empty": "Es gibt keine Items zum Anzeigen", // "collection.page.edit": "Edit this collection", "collection.page.edit": "Diese Sammlung bearbeiten", @@ -1362,8 +1358,7 @@ "comcol-role.edit.create": "Erstellen", // "comcol-role.edit.restrict": "Restrict", - // TODO New key - Add a translation - "comcol-role.edit.restrict": "Restrict", + "comcol-role.edit.restrict": "Beschränken", // "comcol-role.edit.delete": "Delete", "comcol-role.edit.delete": "Löschen", @@ -1377,29 +1372,24 @@ // "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).", - // TODO New key - Add a translation - "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.community-admin.description": "Community-Administrator:innen können Teilbereiche oder Sammlungen erstellen, verwalten oder die Verwaltung anderen zuweisen. Darüber hinaus entscheiden sie, wer Items zu einer Teilsammlung einreichen, Item-Metadaten bearbeiten (nach der Einreichung) und bestehende Items von anderen Sammlungen verknüpfen kann (vorbehaltlich einer Berechtigung).", // "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).", - // TODO New key - Add a translation - "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.collection-admin.description": "Sammlungsadministrator:innen entscheiden, wer Items zu einer Sammlung einreichen, Item-Metadaten bearbeiten (nach der Einreichung) and und bestehende Items von anderen Sammlungen mit dieser Sammlung verknüpfen kann (vorbehaltlich einer Berechtigung für diese Sammlung).", // "comcol-role.edit.submitters.name": "Submitters", "comcol-role.edit.submitters.name": "Einreichende", // "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", - // TODO New key - Add a translation - "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", + "comcol-role.edit.submitters.description": "Die Personen und Gruppen, die die Berechtigungen haben, neue Items zu dieser Sammlung einzureichen.", // "comcol-role.edit.item_read.name": "Default item read access", - // TODO New key - Add a translation - "comcol-role.edit.item_read.name": "Default item read access", + "comcol-role.edit.item_read.name": "Standard-Lesezugriff Item", // "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.", - // TODO New key - Add a translation - "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.description": "Personen und Gruppen, die neu eingereichte Items dieser Sammlung lesen können. Änderungen an dieser Rolle sind nicht rückwirkend. Bestehende Items im System können weiterhin von denjenigen eingesehen werden, die zum Zeitpunkt des Hinzufügens Lesezugriff hatten.", // "comcol-role.edit.item_read.anonymous-group": "Default read for incoming items is currently set to Anonymous.", "comcol-role.edit.item_read.anonymous-group": "Die Standardeinstellung für das Lesen eingehender Items ist derzeit auf Anonym eingestellt.", @@ -1409,8 +1399,7 @@ "comcol-role.edit.bitstream_read.name": "Standard-Lesezugriff Datei", // "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).", - // TODO New key - Add a translation - "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.description": "Community-Administrator:innen können Teilbereiche oder Sammlungen erstellen, verwalten oder die Verwaltung anderen zuweisen. Darüber hinaus entscheiden sie, wer Items zu einer Teilsammlung einreichen, Item-Metadaten bearbeiten (nach der Einreichung) und bestehende Items von anderen Sammlungen verknüpfen kann (vorbehaltlich einer Berechtigung).", // "comcol-role.edit.bitstream_read.anonymous-group": "Default read for incoming bitstreams is currently set to Anonymous.", "comcol-role.edit.bitstream_read.anonymous-group": "Die Standardeinstellung für das Lesen eingehender Dateien ist derzeit auf Anonym eingestellt.", @@ -1486,11 +1475,9 @@ "cookies.consent.accept-selected": "Ausgewählte annehmen", // "cookies.consent.app.opt-out.description": "This app is loaded by default (but you can opt out)", - // TODO New key - Add a translation - "cookies.consent.app.opt-out.description": "This app is loaded by default (but you can opt out)", + "cookies.consent.app.opt-out.description": "Diese App wird standardmäßig geladen (Sie können dies jedoch deaktivieren)", // "cookies.consent.app.opt-out.title": "(opt-out)", - // TODO New key - Add a translation "cookies.consent.app.opt-out.title": "(opt-out)", // "cookies.consent.app.purpose": "purpose", @@ -1512,8 +1499,7 @@ "cookies.consent.decline": "Ablehnen", // "cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: Authentication, Preferences, Acknowledgement and Statistics.
    To learn more, please read our {privacyPolicy}.", - // TODO New key - Add a translation - "cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: Authentication, Preferences, Acknowledgement and Statistics.
    To learn more, please read our {privacyPolicy}.", + "cookies.consent.content-notice.description": "Wir sammeln und verarbeiten Ihre personenbezogenen Daten für die folgenden Zwecke: Authentication, Preferences, Acknowledgement and Statistics.
    Um mehr zu erfahren, lesen Sie bitte unsere {privacyPolicy}.", // "cookies.consent.content-notice.learnMore": "Customize", "cookies.consent.content-notice.learnMore": "Anpassen", @@ -1574,12 +1560,10 @@ "curation-task.task.checklinks.label": "Links in Metadaten prüfen", // "curation-task.task.noop.label": "NOOP", - // TODO New key - Add a translation "curation-task.task.noop.label": "NOOP", // "curation-task.task.profileformats.label": "Profile Bitstream Formats", - // TODO New key - Add a translation - "curation-task.task.profileformats.label": "Profile Bitstream Formats", + "curation-task.task.profileformats.label": "Dateiformate auflisten", // "curation-task.task.requiredmetadata.label": "Check for Required Metadata", "curation-task.task.requiredmetadata.label": "Prüfung auf erforderliche Metadaten", @@ -1614,8 +1598,7 @@ "curation.form.handle.label": "Handle:", // "curation.form.handle.hint": "Hint: Enter [your-handle-prefix]/0 to run a task across entire site (not all tasks may support this capability)", - // TODO New key - Add a translation - "curation.form.handle.hint": "Hint: Enter [your-handle-prefix]/0 to run a task across entire site (not all tasks may support this capability)", + "curation.form.handle.hint": "Hinweis: Geben Sie [your-handle-prefix]/0 ein, um eine Aufgabe für die gesamte Seite durchzuführen (nicht alle Aufgaben unterstützen diese Funktion)", @@ -1635,7 +1618,7 @@ "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.create.item.head": "Neues Item", // "dso-selector.create.item.sub-level": "Create a new item in", "dso-selector.create.item.sub-level": "Ein neues Item erstellen in", @@ -1650,7 +1633,7 @@ "dso-selector.edit.community.head": "Bereich bearbeiten", // "dso-selector.edit.item.head": "Edit item", - "dso-selector.edit.item.head": "Ressource bearbeiten", + "dso-selector.edit.item.head": "Item bearbeiten", // "dso-selector.export-metadata.dspaceobject.head": "Export metadata from", "dso-selector.export-metadata.dspaceobject.head": "Exportieren der Metadaten aus", @@ -1689,11 +1672,10 @@ // "error.bitstream": "Error fetching bitstream", - // TODO New key - Add a translation - "error.bitstream": "Error fetching bitstream", + "error.bitstream": "Fehler beim Laden der Datei", // "error.browse-by": "Error fetching items", - "error.browse-by": "Fehler beim Laden der Ressourcen", + "error.browse-by": "Fehler beim Laden der Items", // "error.collection": "Error fetching collection", "error.collection": "Fehler beim Laden der Sammlung", @@ -1705,16 +1687,16 @@ "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.identifier": "Zu dieser Kennung wurde kein Item gefunden", // "error.default": "Error", "error.default": "Fehler", // "error.item": "Error fetching item", - "error.item": "Fehler beim Laden der Ressource", + "error.item": "Fehler beim Laden des Items", // "error.items": "Error fetching items", - "error.items": "Fehler beim Laden der Ressourcen", + "error.items": "Fehler beim Laden der Items", // "error.objects": "Error fetching objects", "error.objects": "Fehler beim Laden der Objekte", @@ -1777,8 +1759,7 @@ "forgot-email.form.header": "Passwort vergessen", // "forgot-email.form.info": "Enter Register an account to subscribe to collections for email updates, and submit new items to DSpace.", - // TODO New key - Add a translation - "forgot-email.form.info": "Enter Register an account to subscribe to collections for email updates, and submit new items to DSpace.", + "forgot-email.form.info": "Geben Sie einen Account ein, um Sammlungen für E-Mail-Updates zu abonnieren und neue Items an DSpace zu übermitteln.", // "forgot-email.form.email": "Email Address *", "forgot-email.form.email": "E-Mail-Addresse *", @@ -1799,14 +1780,12 @@ "forgot-email.form.success.head": "Verifizierungs-E-Mail gesendet", // "forgot-email.form.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", - // TODO New key - Add a translation - "forgot-email.form.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", + "forgot-email.form.success.content": "Es wurde eine E-Mail mit einer speziellen URL und weiteren Anweisungen an {{ email }} gesendet.", // "forgot-email.form.error.head": "Error when trying to register email", "forgot-email.form.error.head": "Fehler beim Versuch, die E-Mail zu registrieren", // "forgot-email.form.error.content": "An error occured when registering the following email address: {{ email }}", - // TODO New key - Add a translation "forgot-email.form.error.content": "Bei der Registrierung der folgenden E-Mail-Adresse ist ein Fehler aufgetreten: {{ email }}", @@ -1999,8 +1978,7 @@ // "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", - // TODO New key - Add a translation - "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", + "item.edit.authorizations.heading": "Mit diesem Editor können Sie die Richtlinien eines Items anzeigen und ändern sowie die Richtlinien der einzelnen Itemkomponenten, d. h. Bündel und Dateien, ändern. Kurz gesagt, ein Item ist ein Container von Bündeln, und Bündel sind Container von Dateien. Container haben normalerweise ADD/REMOVE/READ/WRITE-Richtlinien, während Dateien nur READ/WRITE-Richtlinien haben.", // "item.edit.authorizations.title": "Edit item's Policies", "item.edit.authorizations.title": "Item-Richtlinien bearbeiten", @@ -2025,8 +2003,7 @@ "item.bitstreams.upload.bundle.new": "Bündel erstellen", // "item.bitstreams.upload.bundles.empty": "This item doesn\'t contain any bundles to upload a bitstream to.", - // TODO New key - Add a translation - "item.bitstreams.upload.bundles.empty": "This item doesn\'t contain any bundles to upload a bitstream to.", + "item.bitstreams.upload.bundles.empty": "Dieses Item enthält keine Bündel, um eine Datei hochzuladen.", // "item.bitstreams.upload.cancel": "Cancel", "item.bitstreams.upload.cancel": "Abbrechen", @@ -2055,8 +2032,7 @@ "item.edit.bitstreams.bundle.edit.buttons.upload": "Hochladen", // "item.edit.bitstreams.bundle.displaying": "Currently displaying {{ amount }} bitstreams of {{ total }}.", - // TODO New key - Add a translation - "item.edit.bitstreams.bundle.displaying": "Currently displaying {{ amount }} bitstreams of {{ total }}.", + "item.edit.bitstreams.bundle.displaying": "Derzeit werden {{ amount }} Dateien von insgesamt {{ total }} angezeigt.", // "item.edit.bitstreams.bundle.load.all": "Load all ({{ total }})", "item.edit.bitstreams.bundle.load.all": "Alle ({{ total }}) laden", @@ -2086,7 +2062,7 @@ "item.edit.bitstreams.edit.buttons.undo": "Änderungen rückgängig machen", // "item.edit.bitstreams.empty": "This item doesn't contain any bitstreams. Click the upload button to create one.", - "item.edit.bitstreams.empty": "Diese Ressource enthält keine Dateien. Klicken Sie auf das Feld Hochladen, um eine zu erstellen.", + "item.edit.bitstreams.empty": "Dieses Item enthält keine Dateien. Klicken Sie auf das Feld Hochladen, um eine zu erstellen.", // "item.edit.bitstreams.headers.actions": "Actions", "item.edit.bitstreams.headers.actions": "Aktionen", @@ -2113,15 +2089,13 @@ "item.edit.bitstreams.notifications.move.failed.title": "Fehler beim Verschieben von Dateien", // "item.edit.bitstreams.notifications.move.saved.content": "Your move changes to this item's bitstreams and bundles have been saved.", - // TODO New key - Add a translation - "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.content": "Die von Ihnen vorgenommenen Änderungen an den Dateien und Bündeln dieses Artikels wurden gespeichert.", // "item.edit.bitstreams.notifications.move.saved.title": "Move changes saved", - // TODO New key - Add a translation - "item.edit.bitstreams.notifications.move.saved.title": "Move changes saved", + "item.edit.bitstreams.notifications.move.saved.title": "Änderungen gespeichert", // "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.content": "Die Ressource, an der Sie gerade arbeiten, wurde von einem anderen Account geändert. Ihre aktuellen Änderungen werden verworfen, um Konflikte zu vermeiden.", + "item.edit.bitstreams.notifications.outdated.content": "Das Item, an der Sie gerade arbeiten, wurde von einem anderen Account geändert. Ihre aktuellen Änderungen werden verworfen, um Konflikte zu vermeiden.", // "item.edit.bitstreams.notifications.outdated.title": "Changes outdated", "item.edit.bitstreams.notifications.outdated.title": "Veraltete Änderungen", @@ -2130,12 +2104,10 @@ "item.edit.bitstreams.notifications.remove.failed.title": "Fehler beim Löschen der Datei", // "item.edit.bitstreams.notifications.remove.saved.content": "Your removal changes to this item's bitstreams have been saved.", - // TODO New key - Add a translation - "item.edit.bitstreams.notifications.remove.saved.content": "Your removal changes to this item's bitstreams have been saved.", + "item.edit.bitstreams.notifications.remove.saved.content": "Ihre Entfernungsänderungen an den Dateien dieses Items wurden gespeichert.", // "item.edit.bitstreams.notifications.remove.saved.title": "Removal changes saved", - // TODO New key - Add a translation - "item.edit.bitstreams.notifications.remove.saved.title": "Removal changes saved", + "item.edit.bitstreams.notifications.remove.saved.title": "Änderungen gespeichert", // "item.edit.bitstreams.reinstate-button": "Undo", "item.edit.bitstreams.reinstate-button": "Rückgängig", @@ -2155,59 +2127,59 @@ "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.description": "Sind Sie sicher, dass dieses Item 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.header": "Löschen des Items: {{ id }}", // "item.edit.delete.success": "The item has been deleted", - "item.edit.delete.success": "Die Ressource wurde gelöscht", + "item.edit.delete.success": "Das Item wurde gelöscht", // "item.edit.head": "Edit Item", - "item.edit.head": "Ressource bearbeiten", + "item.edit.head": "Item bearbeiten", // "item.edit.breadcrumbs": "Edit Item", - "item.edit.breadcrumbs": "Ressource bearbeiten", + "item.edit.breadcrumbs": "Item bearbeiten", // "item.edit.tabs.mapper.head": "Collection Mapper", "item.edit.tabs.mapper.head": "Spiegeln", // "item.edit.tabs.item-mapper.title": "Item Edit - Collection Mapper", - "item.edit.tabs.item-mapper.title": "Ressource bearbeiten - Spiegeln", + "item.edit.tabs.item-mapper.title": "Item bearbeiten - Spiegeln", // "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.add": "Item 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.buttons.remove": "Spiegelung des Items 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": "Sammlungsadministrator:innen 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.description": "Sammlungsadministrator:innen haben die Möglichkeit Items von einer Sammlung in eine andere zu spiegeln. Man kann nach Items in anderen Sammlungen suchen und diese spiegeln oder sich eine Liste der gespiegelten Items anzeigen lassen.", // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", - "item.edit.item-mapper.head": "Ressourcen spiegeln - Spiegelt eine Ressource in andere Sammlungen", + "item.edit.item-mapper.head": "Item spiegeln - Spiegelt ein Item in andere Sammlungen", // "item.edit.item-mapper.item": "Item: \"{{name}}\"", - "item.edit.item-mapper.item": "Ressource: \"{{name}}\"", + "item.edit.item-mapper.item": "Item: \"{{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 Ressource in {{amount}} Sammlung(en) ist ein Fehler aufgetreten.", + "item.edit.item-mapper.notifications.add.error.content": "Beim Spiegeln des Items 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.content": "Item 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", @@ -2216,10 +2188,10 @@ "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.error.head": "Fehler beim Entfernen von gespiegelten Items", // "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.content": "Spiegelung des Items 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", @@ -2251,7 +2223,7 @@ "item.edit.metadata.edit.buttons.unedit": "Bearbeitung beenden", // "item.edit.metadata.empty": "The item currently doesn't contain any metadata. Click Add to start adding a metadata value.", - "item.edit.metadata.empty": "Die Ressource enthält derzeit keine Metadaten. Klicken Sie auf Hinzufügen, um einen Metadatenwert hinzuzufügen.", + "item.edit.metadata.empty": "Das Item enthält derzeit keine Metadaten. Klicken Sie auf Hinzufügen, um einen Metadatenwert hinzuzufügen.", // "item.edit.metadata.headers.edit": "Edit", "item.edit.metadata.headers.edit": "Bearbeiten", @@ -2284,13 +2256,13 @@ "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 Account geändert. Ihre aktuellen Änderungen werden nicht angewandt, um Konflikte zu vermeiden.", + "item.edit.metadata.notifications.outdated.content": "Das Item, an dem Sie gerade arbeiten, wurde von einem anderen Account 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.content": "Ihre Änderungen an den Metadaten des Items wurden gespeichert.", // "item.edit.metadata.notifications.saved.title": "Metadata saved", "item.edit.metadata.notifications.saved.title": "Metadaten gespeichert", @@ -2321,10 +2293,10 @@ "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.error": "Ein Fehler ist beim Verschieben des Items aufgetreten", // "item.edit.move.head": "Move item: {{id}}", - "item.edit.move.head": "Ressource verschieben: {{id}}", + "item.edit.move.head": "Item verschieben: {{id}}", // "item.edit.move.inheritpolicies.checkbox": "Inherit policies", "item.edit.move.inheritpolicies.checkbox": "Rechte erben", @@ -2342,10 +2314,10 @@ "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.success": "Das Item wurde erfolgreich verschoben", // "item.edit.move.title": "Move item", - "item.edit.move.title": "Ressource verschieben", + "item.edit.move.title": "Item verschieben", @@ -2356,16 +2328,16 @@ "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.description": "Wollen Sie dieses Item 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.error": "Ein Fehler ist aufgetreten bei dem Versuch, das Item privat zu machen", // "item.edit.private.header": "Make item private: {{ id }}", - "item.edit.private.header": "Ressource: {{ id }} privat machen", + "item.edit.private.header": "Item: {{ id }} privat machen", // "item.edit.private.success": "The item is now private", - "item.edit.private.success": "Diese Ressource ist nun privat", + "item.edit.private.success": "Dieses Item ist nun privat", @@ -2376,16 +2348,16 @@ "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, dass diese Ressource im Repositorium öffentlich gemacht werden soll?", + "item.edit.public.description": "Sind Sie sicher, dass dieses Item 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.error": "Ein Fehler ist aufgetreten, als das Item öffentlich gemacht werden sollte.", // "item.edit.public.header": "Make item public: {{ id }}", - "item.edit.public.header": "Ressource: {{ id }} öffentlich machen", + "item.edit.public.header": "Item: {{ id }} öffentlich machen", // "item.edit.public.success": "The item is now public", - "item.edit.public.success": "Die Ressource ist nun öffentlich", + "item.edit.public.success": "Das Item ist nun öffentlich", @@ -2396,16 +2368,16 @@ "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.description": "Sind Sie sicher, dass das Item 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.error": "Ein Fehler ist bei der Reinstantiierung des Items aufgetreten.", // "item.edit.reinstate.header": "Reinstate item: {{ id }}", - "item.edit.reinstate.header": "Ressource: {{ id }} reinstantiieren", + "item.edit.reinstate.header": "Item: {{ id }} reinstantiieren", // "item.edit.reinstate.success": "The item was reinstated successfully", - "item.edit.reinstate.success": "Die Ressource wurde erfolgreich reinstantiiert", + "item.edit.reinstate.success": "Das Item wurde erfolgreich reinstantiiert", @@ -2434,13 +2406,13 @@ "item.edit.relationships.notifications.failed.title": "Fehler beim Bearbeiten 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 Account geändert. Ihre aktuellen Änderungen werden verworfen, um Konflikte zu vermeiden.", + "item.edit.relationships.notifications.outdated.content": "Das Item, das Sie gerade bearbeiten, wurde von einem anderen Account 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.content": "Ihre Änderungen an den Beziehungen des Items wurden gespeichert.", // "item.edit.relationships.notifications.saved.title": "Relationships saved", "item.edit.relationships.notifications.saved.title": "Beziehungen gespeichert", @@ -2452,40 +2424,39 @@ "item.edit.relationships.save-button": "Speichern", // "item.edit.relationships.no-entity-type": "Add 'dspace.entity.type' metadata to enable relationships for this item", - "item.edit.relationships.no-entity-type": "Füge 'dspace.entity.type' in die Metadaten hinzu, um Beziehungen für diese Ressource zu ermöglichen", + "item.edit.relationships.no-entity-type": "Füge 'dspace.entity.type' in die Metadaten hinzu, um Beziehungen für dieses Item zu ermöglichen", // "item.edit.tabs.bitstreams.head": "Bitstreams", - // TODO Source message changed - Revise the translation "item.edit.tabs.bitstreams.head": "Dateien", // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", - "item.edit.tabs.bitstreams.title": "Ressource bearbeiten - Dateien", + "item.edit.tabs.bitstreams.title": "Item 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.curate.title": "Item bearbeiten - Pflegen", // "item.edit.tabs.metadata.head": "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.metadata.title": "Item bearbeiten - Metadaten", // "item.edit.tabs.relationships.head": "Relationships", "item.edit.tabs.relationships.head": "Beziehungen", // "item.edit.tabs.relationships.title": "Item Edit - Relationships", - "item.edit.tabs.relationships.title": "Ressource bearbeiten - Relationen", + "item.edit.tabs.relationships.title": "Item bearbeiten - Relationen", // "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", "item.edit.tabs.status.buttons.authorizations.button": "Berechtigungen...", // "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.authorizations.label": "Rechte des Items bearbeiten", // "item.edit.tabs.status.buttons.delete.button": "Permanently delete", "item.edit.tabs.status.buttons.delete.button": "Endgültig löschen", @@ -2503,34 +2474,34 @@ "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.move.label": "Item 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": "Ressource privat machen", + "item.edit.tabs.status.buttons.private.label": "Item 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.public.label": "Item ö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.reinstate.label": "Item 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.buttons.withdraw.label": "Item 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.description": "Item bearbeiten. Von hier können Sie Items 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": "Status", "item.edit.tabs.status.head": "Status", @@ -2539,31 +2510,31 @@ "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.id": "Interne ID des Items", // "item.edit.tabs.status.labels.itemPage": "Item Page", - "item.edit.tabs.status.labels.itemPage": "Startseite der Ressource", + "item.edit.tabs.status.labels.itemPage": "Startseite des Items", // "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.status.title": "Item bearbeiten - Status", // "item.edit.tabs.versionhistory.head": "Version History", "item.edit.tabs.versionhistory.head": "Versionsgeschichte", // "item.edit.tabs.versionhistory.title": "Item Edit - Version History", - "item.edit.tabs.versionhistory.title": "Ressource bearbeiten - Versionsgeschichte", + "item.edit.tabs.versionhistory.title": "Item bearbeiten - Versionsgeschichte", // "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": "Das Bearbeiten oder Hinzufügen neuer Versionen ist in dieser Benutzeroberfläche noch nicht möglich.", // "item.edit.tabs.view.head": "View Item", - "item.edit.tabs.view.head": "Ressource ansehen", + "item.edit.tabs.view.head": "Item ansehen", // "item.edit.tabs.view.title": "Item Edit - View", - "item.edit.tabs.view.title": "Ressource bearbeiten - Ansicht", + "item.edit.tabs.view.title": "Item bearbeiten - Ansicht", @@ -2574,27 +2545,27 @@ "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.description": "Sind Sie sicher, dass Sie dieses Item 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.error": "Ein Fehler ist beim Zurückziehen des Items aufgetreten", // "item.edit.withdraw.header": "Withdraw item: {{ id }}", - "item.edit.withdraw.header": "Ressource: {{ id }} zurückziehen", + "item.edit.withdraw.header": "Item: {{ id }} zurückziehen", // "item.edit.withdraw.success": "The item was withdrawn successfully", - "item.edit.withdraw.success": "Die Ressource wurde erfolgreich zurückgezogen", + "item.edit.withdraw.success": "Das Item wurde erfolgreich zurückgezogen", // "item.listelement.badge": "Item", - "item.listelement.badge": "Ressource", + "item.listelement.badge": "Item", // "item.page.description": "Description", "item.page.description": "Beschreibung", // "item.page.edit": "Edit this item", - "item.page.edit": "Diese Ressource bearbeiten", + "item.page.edit": "Dieses Item bearbeiten", // "item.page.journal-issn": "Journal ISSN", "item.page.journal-issn": "ISSN der Zeitschrift", @@ -2606,16 +2577,16 @@ "item.page.publisher": "Verlag", // "item.page.titleprefix": "Item: ", - "item.page.titleprefix": "Ressource: ", + "item.page.titleprefix": "Item: ", // "item.page.volume-title": "Volume Title", "item.page.volume-title": "Bandtitel", // "item.search.results.head": "Item Search Results", - "item.search.results.head": "Ressourcen Suchergebnisse", + "item.search.results.head": "Item Suchergebnisse", // "item.search.title": "DSpace Angular :: Item Search", - "item.search.title": "DSpace Angular :: Ressourcen-Suche", + "item.search.title": "DSpace Angular :: Item-Suche", @@ -2635,7 +2606,7 @@ "item.page.date": "Datum", // "item.page.edit": "Edit this item", - "item.page.edit": "Diese Ressource bearbeiten", + "item.page.edit": "Dieses Item bearbeiten", // "item.page.files": "Files", "item.page.files": "Dateien", @@ -2728,7 +2699,6 @@ "item.preview.dc.title": "Titel:", // "item.preview.person.familyName": "Surname:", - // TODO New key - Add a translation "item.preview.person.familyName": "Nachname:", // "item.preview.person.givenName": "Name:", @@ -2742,7 +2712,7 @@ "item.select.confirm": "Auswahl bestätigen", // "item.select.empty": "No items to show", - "item.select.empty": "Es gibt keine Ressourcen dazu", + "item.select.empty": "Es gibt keine Items dazu", // "item.select.table.author": "Author", "item.select.table.author": "Autor:in", @@ -2755,7 +2725,7 @@ // "item.version.history.empty": "There are no other versions for this item yet.", - "item.version.history.empty": "Es gibt noch keine anderen Versionen für diese Ressource.", + "item.version.history.empty": "Es gibt noch keine anderen Versionen für dieses Item.", // "item.version.history.head": "Version History", "item.version.history.head": "Versionsgeschichte", @@ -2770,7 +2740,7 @@ "item.version.history.table.version": "Version", // "item.version.history.table.item": "Item", - "item.version.history.table.item": "Ressource", + "item.version.history.table.item": "Item", // "item.version.history.table.editor": "Editor", "item.version.history.table.editor": "Herausgeber:in", @@ -2784,8 +2754,7 @@ // "item.version.notice": "This is not the latest version of this item. The latest version can be found here.", - // TODO New key - Add a translation - "item.version.notice": "Dies ist nicht die neueste Version dieses Artikels. Die neueste Version finden Sie hier.", + "item.version.notice": "Dies ist nicht die neueste Version dieses Items. Die neueste Version finden Sie unter hier.", @@ -2796,7 +2765,7 @@ "journal.page.description": "Beschreibung", // "journal.page.edit": "Edit this item", - "journal.page.edit": "Diese Ressource bearbeiten", + "journal.page.edit": "Dieses Item bearbeiten", // "journal.page.editor": "Editor-in-Chief", "journal.page.editor": "Chefredakteur:in", @@ -2825,7 +2794,7 @@ "journalissue.page.description": "Beschreibeung", // "journalissue.page.edit": "Edit this item", - "journalissue.page.edit": "Diese Ressource bearbeiten", + "journalissue.page.edit": "Dieses Item bearbeiten", // "journalissue.page.issuedate": "Issue Date", "journalissue.page.issuedate": "Erscheinungsdatum", @@ -2854,7 +2823,7 @@ "journalvolume.page.description": "Beschreibung", // "journalvolume.page.edit": "Edit this item", - "journalvolume.page.edit": "Diese Ressource bearbeiten", + "journalvolume.page.edit": "Dieses Item bearbeiten", // "journalvolume.page.issuedate": "Issue Date", "journalvolume.page.issuedate": "Erscheinungsdatum", @@ -2874,7 +2843,7 @@ "loading.bitstreams": "Lade Datei...", // "loading.browse-by": "Loading items...", - "loading.browse-by": "Lade Ressourcen...", + "loading.browse-by": "Lade Items...", // "loading.browse-by-page": "Loading page...", "loading.browse-by-page": "Lade Seite...", @@ -2895,13 +2864,13 @@ "loading.default": "Lade...", // "loading.item": "Loading item...", - "loading.item": "Lade Ressource...", + "loading.item": "Lade Item...", // "loading.items": "Loading items...", - "loading.items": "Lade Ressourcen...", + "loading.items": "Lade Items...", // "loading.mydspace-results": "Loading items...", - "loading.mydspace-results": "Lade Ressourcen...", + "loading.mydspace-results": "Lade Items...", // "loading.objects": "Loading...", "loading.objects": "Lade...", @@ -3043,7 +3012,7 @@ "menu.section.edit_community": "Bereich", // "menu.section.edit_item": "Item", - "menu.section.edit_item": "Ressource", + "menu.section.edit_item": "Item", @@ -3057,7 +3026,7 @@ "menu.section.export_community": "Bereich", // "menu.section.export_item": "Item", - "menu.section.export_item": "Ressource", + "menu.section.export_item": "Item", // "menu.section.export_metadata": "Metadata", "menu.section.export_metadata": "Metadaten", @@ -3068,8 +3037,7 @@ "menu.section.icon.access_control": "Menübereich Zugriffskontrolle", // "menu.section.icon.admin_search": "Admin search menu section", - // TODO New key - Add a translation - "menu.section.icon.admin_search": "Admin search menu section", + "menu.section.icon.admin_search": "Menübereich Admin-Suche", // "menu.section.icon.control_panel": "Control Panel menu section", "menu.section.icon.control_panel": "Menübereich Kontrollfeld", @@ -3130,10 +3098,10 @@ "menu.section.new_community": "Bereich", // "menu.section.new_item": "Item", - "menu.section.new_item": "Ressource", + "menu.section.new_item": "Item", // "menu.section.new_item_version": "Item Version", - "menu.section.new_item_version": "Ressourcenversion", + "menu.section.new_item_version": "Item-Version", // "menu.section.new_process": "Process", "menu.section.new_process": "Prozess", @@ -3214,7 +3182,7 @@ "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.controller-help": "Wählen Sie diese Option, um dem/derjenigen, die das Item eingereicht hat, eine Nachricht zu schicken.", // "mydspace.messages.description-placeholder": "Insert your message here...", "mydspace.messages.description-placeholder": "Geben Sie Ihre Nachricht hier ein...", @@ -3280,7 +3248,7 @@ "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-results": "Es gibt keine Items anzuzeigen", // "mydspace.results.no-title": "No title", "mydspace.results.no-title": "Kein Titel", @@ -3322,10 +3290,10 @@ "mydspace.upload.upload-failed-moreonefile": "Unverarbeitbare Anfrage. Nur eine Datei ist erlaubt.", // "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", - "mydspace.upload.upload-multiple-successful": "{{qty}} neue(s) Arbeitsbereichressource(n) angelegt.", + "mydspace.upload.upload-multiple-successful": "{{qty}} neue(s) Arbeitsbereichitem(s) angelegt.", // "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", - "mydspace.upload.upload-successful": "Neue Arbeitsbereichressource angelegt. Klicken Sie {{here}}, um sie zu bearbeiten.", + "mydspace.upload.upload-successful": "Neues Arbeitsbereichitem angelegt. Klicken Sie {{here}}, um sie zu bearbeiten.", // "mydspace.view-btn": "View", "mydspace.view-btn": "Anzeige", @@ -3380,7 +3348,7 @@ "orgunit.page.description": "Beschreibung", // "orgunit.page.edit": "Edit this item", - "orgunit.page.edit": "Diese Ressourcen bearbeiten", + "orgunit.page.edit": "Dieses Item bearbeiten", // "orgunit.page.id": "ID", "orgunit.page.id": "ID", @@ -3414,7 +3382,7 @@ "person.page.birthdate": "Geburtsdatum", // "person.page.edit": "Edit this item", - "person.page.edit": "Diese Ressource bearbeiten", + "person.page.edit": "Dieses Item bearbeiten", // "person.page.email": "Email Address", "person.page.email": "E-Mail-Adresse", @@ -3517,24 +3485,19 @@ "process.detail.back" : "Zurück", // "process.detail.output" : "Process Output", - // TODO New key - Add a translation - "process.detail.output" : "Process Output", + "process.detail.output" : "Prozessausgabe", // "process.detail.logs.button": "Retrieve process output", - // TODO New key - Add a translation - "process.detail.logs.button": "Retrieve process output", + "process.detail.logs.button": "Abrufen der Prozessausgabe", // "process.detail.logs.loading": "Retrieving", - // TODO New key - Add a translation - "process.detail.logs.loading": "Retrieving", + "process.detail.logs.loading": "Abrufen", // "process.detail.logs.none": "This process has no output", - // TODO New key - Add a translation - "process.detail.logs.none": "This process has no output", + "process.detail.logs.none": "Dieser Prozess hat keine Ausgabe", // "process.detail.output-files" : "Output Files", - // TODO New key - Add a translation - "process.detail.output-files" : "Output Files", + "process.detail.output-files" : "Ausgabedateien", // "process.detail.output-files.empty" : "This process doesn't contain any output files", "process.detail.output-files.empty" : "Dieser Prozess enthält keine Ausgabedateien", @@ -3683,7 +3646,7 @@ "project.page.description": "Beschreibung", // "project.page.edit": "Edit this item", - "project.page.edit": "Diese Ressource bearbeiten", + "project.page.edit": "Dieses Item bearbeiten", // "project.page.expectedcompletion": "Expected Completion", "project.page.expectedcompletion": "Erwartetes Abschlussdatum", @@ -3715,7 +3678,7 @@ "publication.page.description": "Beschreibung", // "publication.page.edit": "Edit this item", - "publication.page.edit": "Diese Ressource bearbeiten", + "publication.page.edit": "Dieses Item bearbeiten", // "publication.page.journal-issn": "Journal ISSN", "publication.page.journal-issn": "ISSN der Zeitschrift", @@ -3810,7 +3773,7 @@ "register-page.registration.header": "Registrierung neuer Benutzer:innen", // "register-page.registration.info": "Register an account to subscribe to collections for email updates, and submit new items to DSpace.", - "register-page.registration.info": "Registrieren Sie ein Konto, um Sammlungen für E-Mail-Updates zu abonnieren und neue Ressourcen in DSpace einzugeben.", + "register-page.registration.info": "Registrieren Sie ein Konto, um Sammlungen für E-Mail-Updates zu abonnieren und neue Items in DSpace einzugeben.", // "register-page.registration.email": "Email Address *", "register-page.registration.email": "E-Mail-Adresse *", @@ -3842,7 +3805,7 @@ // "relationships.add.error.relationship-type.content": "No suitable match could be found for relationship type {{ type }} between the two items", - "relationships.add.error.relationship-type.content": "Für den Beziehungstyp {{ type }} zwischen den beiden Ressourcen konnte keine passende Übereinstimmung gefunden werden", + "relationships.add.error.relationship-type.content": "Für den Beziehungstyp {{ type }} zwischen den beiden Items konnte keine passende Übereinstimmung gefunden werden", // "relationships.add.error.server.content": "The server returned an error", "relationships.add.error.server.content": "Der Server hat einen Fehler zurückgegeben", @@ -3919,55 +3882,46 @@ "resource-policies.add.for.collection": "Eine neue Sammlungen-Richtlinie hinzufügen", // "resource-policies.create.page.heading": "Create new resource policy for ", - // TODO New key - Add a translation - "resource-policies.create.page.heading": "Create new resource policy for ", + "resource-policies.create.page.heading": "Eine neue Ressourcen-Richtlinie erstellen für ", // "resource-policies.create.page.failure.content": "An error occurred while creating the resource policy.", - // TODO New key - Add a translation - "resource-policies.create.page.failure.content": "An error occurred while creating the resource policy.", + "resource-policies.create.page.failure.content": "Bei der Erstellung der Ressourcen-Richtlinie ist ein Fehler aufgetreten.", // "resource-policies.create.page.success.content": "Operation successful", "resource-policies.create.page.success.content": "Vorgang erfolgreich", // "resource-policies.create.page.title": "Create new resource policy", - // TODO New key - Add a translation - "resource-policies.create.page.title": "Create new resource policy", + "resource-policies.create.page.title": "Eine neue Ressourcen-Richtlinie erstellen", // "resource-policies.delete.btn": "Delete selected", "resource-policies.delete.btn": "Ausgewählte löschen", // "resource-policies.delete.btn.title": "Delete selected resource policies", - // TODO New key - Add a translation - "resource-policies.delete.btn.title": "Delete selected resource policies", + "resource-policies.delete.btn.title": "Ausgewählte Ressourcen-Richtlinien löschen", // "resource-policies.delete.failure.content": "An error occurred while deleting selected resource policies.", - // TODO New key - Add a translation - "resource-policies.delete.failure.content": "An error occurred while deleting selected resource policies.", + "resource-policies.delete.failure.content": "Beim Löschen der ausgewählten Ressourcen-Richtlinien ist ein Fehler aufgetreten.", // "resource-policies.delete.success.content": "Operation successful", "resource-policies.delete.success.content": "Vorgang erfolgreich", // "resource-policies.edit.page.heading": "Edit resource policy ", - // TODO New key - Add a translation - "resource-policies.edit.page.heading": "Edit resource policy ", + "resource-policies.edit.page.heading": "Ressourcen-Richtlinie bearbeiten ", // "resource-policies.edit.page.failure.content": "An error occurred while editing the resource policy.", - // TODO New key - Add a translation - "resource-policies.edit.page.failure.content": "An error occurred while editing the resource policy.", + "resource-policies.edit.page.failure.content": "Beim Bearbeiten der Ressourcen-Richtlinie ist ein Fehler aufgetreten.", // "resource-policies.edit.page.success.content": "Operation successful", "resource-policies.edit.page.success.content": "Vorgang erfolgreich", // "resource-policies.edit.page.title": "Edit resource policy", - // TODO New key - Add a translation - "resource-policies.edit.page.title": "Edit resource policy", + "resource-policies.edit.page.title": "Ressourcen-Richtlinie bearbeiten", // "resource-policies.form.action-type.label": "Select the action type", "resource-policies.form.action-type.label": "Wählen Sie die Aktionsart", // "resource-policies.form.action-type.required": "You must select the resource policy action.", - // TODO New key - Add a translation - "resource-policies.form.action-type.required": "You must select the resource policy action.", + "resource-policies.form.action-type.required": "Sie müssen die Aktion der Ressourcen-Richtlinie auswählen.", // "resource-policies.form.eperson-group-list.label": "The eperson or group that will be granted the permission", "resource-policies.form.eperson-group-list.label": "Die Person oder Gruppe, der die Genehmigung erteilt wird", @@ -3982,8 +3936,7 @@ "resource-policies.form.eperson-group-list.tab.group": "Suche nach einer Gruppe", // "resource-policies.form.eperson-group-list.table.headers.action": "Action", - // TODO New key - Add a translation - "resource-policies.form.eperson-group-list.table.headers.action": "Action", + "resource-policies.form.eperson-group-list.table.headers.action": "Aktion", // "resource-policies.form.eperson-group-list.table.headers.id": "ID", "resource-policies.form.eperson-group-list.table.headers.id": "ID", @@ -4007,8 +3960,7 @@ "resource-policies.form.policy-type.label": "Wählen Sie den Richtlinientyp", // "resource-policies.form.policy-type.required": "You must select the resource policy type.", - // TODO New key - Add a translation - "resource-policies.form.policy-type.required": "You must select the resource policy type.", + "resource-policies.form.policy-type.required": "Sie müssen den Typ der Ressourcen-Richtlinie auswählen", // "resource-policies.table.headers.action": "Action", "resource-policies.table.headers.action": "Aktion", @@ -4091,7 +4043,7 @@ "search.filters.applied.f.discoverable": "Privat", // "search.filters.applied.f.entityType": "Item Type", - "search.filters.applied.f.entityType": "Art der Ressource", + "search.filters.applied.f.entityType": "Item-Typ", // "search.filters.applied.f.has_content_in_original_bundle": "Has files", "search.filters.applied.f.has_content_in_original_bundle": "Hat Dateien", @@ -4180,10 +4132,10 @@ "search.filters.filter.withdrawn.head": "Zurückgezogen", // "search.filters.filter.entityType.head": "Item Type", - "search.filters.filter.entityType.head": "Art der Ressource", + "search.filters.filter.entityType.head": "Item-Typ", // "search.filters.filter.entityType.placeholder": "Item Type", - "search.filters.filter.entityType.placeholder": "Art der Ressource", + "search.filters.filter.entityType.placeholder": "Item-Typ", // "search.filters.filter.has_content_in_original_bundle.head": "Has files", "search.filters.filter.has_content_in_original_bundle.head": "Enthält Dateien", @@ -4447,7 +4399,7 @@ "submission.import-external.title": "Importieren von Metadaten aus einer externen Quelle", // "submission.import-external.page.hint": "Enter a query above to find items from the web to import in to DSpace.", - "submission.import-external.page.hint": "Geben Sie oben eine Abfrage ein, um Ressourcen aus dem Internet zu finden, die in DSpace importiert werden sollen.", + "submission.import-external.page.hint": "Geben Sie oben eine Abfrage ein, um Items aus dem Internet zu finden, die in DSpace importiert werden sollen.", // "submission.import-external.back-to-my-dspace": "Back to MyDSpace", "submission.import-external.back-to-my-dspace": "Zurück zu MyDSpace", @@ -4486,7 +4438,7 @@ "submission.import-external.source.lcname": "Library of Congress Names", // "submission.import-external.preview.title": "Item Preview", - "submission.import-external.preview.title": "Ressourcen-Vorschau", + "submission.import-external.preview.title": "Item-Vorschau", // "submission.import-external.preview.subtitle": "The metadata below was imported from an external source. It will be pre-filled when you start the submission.", "submission.import-external.preview.subtitle": "Die folgenden Metadaten wurden aus einer externen Quelle importiert. Sie werden vorausgefüllt, wenn Sie die Eingabe starten.", @@ -4507,8 +4459,7 @@ "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.isAuthorOfPublication": "Import remote author", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.external-source.import-button-title.isAuthorOfPublication": "Import remote author", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.isAuthorOfPublication": "Importiere Autor:innen-Metadaten", // "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", @@ -4520,8 +4471,7 @@ "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Volume": "Zeitschriftenband importieren", // "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.title": "Import Remote Author", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.title": "Import Remote Author", + "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.title": "Importiere Autor:innen-Metadaten", // "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Successfully added local author to the selection", "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Erfolgreicher Import und Hinzufügen eines/einer lokalen Autor:in zur Auswahl", @@ -4620,55 +4570,46 @@ "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.selected": "{{ size }} Items ausgewählt", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Local Authors ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Local Authors ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Lokale Autoren ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Local Journals ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Local Journals ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Lokale Zeitschriften ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Project": "Local Projects ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Project": "Local Projects ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Project": "Lokale Projekte ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Publication": "Local Publications ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Publication": "Local Publications ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Publication": "Lokale Publicationen ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Person": "Local Authors ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Person": "Local Authors ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Person": "Lokale Autoren ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.OrgUnit": "Local Organizational Units ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.OrgUnit": "Local Organizational Units ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.OrgUnit": "Lokale Organisationseinheiten ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataPackage": "Local Data Packages ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataPackage": "Local Data Packages ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataPackage": "Lokale Datenpakete ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataFile": "Local Data Files ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataFile": "Local Data Files ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.DataFile": "Lokale Dateien ({{ 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.isJournalIssueOfPublication": "Local Journal Issues ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalIssueOfPublication": "Local Journal Issues ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalIssueOfPublication": "Lokale Zeitschriftenhefte ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalIssue": "Local Journal Issues ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalIssue": "Local Journal Issues ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalIssue": "Lokale Zeitschriftenhefte ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Local Journal Volumes ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Local Journal Volumes ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Lokale Zeitschriftenbände ({{ count }})", + // "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalVolume": "Local Journal Volumes ({{ count }})", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalVolume": "Local Journal Volumes ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.JournalVolume": "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 }})", @@ -4734,8 +4675,7 @@ "submission.sections.describe.relationship-lookup.title.DataPackage": "Datenpakete", // "submission.sections.describe.relationship-lookup.title.DataFile": "Data Files", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.title.DataFile": "Data Files", + "submission.sections.describe.relationship-lookup.title.DataFile": "Dateien", // "submission.sections.describe.relationship-lookup.title.Funding Agency": "Funding Agency", "submission.sections.describe.relationship-lookup.title.Funding Agency": "Fördereinrichtung", @@ -4780,8 +4720,7 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.DataPackage": "Ausgewählte Datenpakete", // "submission.sections.describe.relationship-lookup.selection-tab.title.DataFile": "Selected Data Files", - // TODO New key - Add a translation - "submission.sections.describe.relationship-lookup.selection-tab.title.DataFile": "Selected Data Files", + "submission.sections.describe.relationship-lookup.selection-tab.title.DataFile": "Ausgewählte Dateien", // "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals", "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Ausgewählte Zeitschriften", @@ -4850,12 +4789,10 @@ "submission.sections.ccLicense.option.select": "Wählen Sie eine Option…", // "submission.sections.ccLicense.link": "You’ve selected the following license:", - // TODO New key - Add a translation - "submission.sections.ccLicense.link": "You’ve selected the following license:", + "submission.sections.ccLicense.link": "Sie haben die folgende Lizenz ausgewählt:", // "submission.sections.ccLicense.confirmation": "I grant the license above", - // TODO New key - Add a translation - "submission.sections.ccLicense.confirmation": "I grant the license above", + "submission.sections.ccLicense.confirmation": "Ich erteile die oben genannte Lizenz", // "submission.sections.general.add-more": "Add more", "submission.sections.general.add-more": "Mehr Hinzufügen", @@ -4864,7 +4801,7 @@ "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 Ressource ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.", + "submission.sections.general.deposit_error_notice": "Beim Einreichen des Items 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", @@ -4888,7 +4825,7 @@ "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_error_notice": "Beim Speichern des Items 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.", @@ -4902,7 +4839,6 @@ // "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.describe.recycle": "Recycle", @@ -4944,7 +4880,7 @@ "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.drop-message": "Dateien herüberziehen, um sie dem Item hinzuzufügen", // "submission.sections.upload.form.access-condition-label": "Access condition type", "submission.sections.upload.form.access-condition-label": "Zugriffsbedingung Typ", @@ -4953,7 +4889,6 @@ "submission.sections.upload.form.date-required": "Datum erforderlich.", // "submission.sections.upload.form.from-label": "Grant access from", - // TODO Source message changed - Revise the translation "submission.sections.upload.form.from-label": "Zugriff gewährt ab", // "submission.sections.upload.form.from-placeholder": "From", @@ -4966,7 +4901,6 @@ "submission.sections.upload.form.group-required": "Gruppe ist erforderlich", // "submission.sections.upload.form.until-label": "Grant access until", - // TODO Source message changed - Revise the translation "submission.sections.upload.form.until-label": "Zugriff gewährt bis", // "submission.sections.upload.form.until-placeholder": "Until", @@ -4979,7 +4913,7 @@ "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.info": "Hier finden Sie alle Dateien, die aktuell zum Item 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", @@ -5010,19 +4944,19 @@ "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.delete-help": "Wenn Sie das Item 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.edit-help": "Wählen Sie diese Option, um die Metadaten des Items 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.generic.view-help": "Wählen Sie diese Option, um die Metadaten des Items anzuzeigen", @@ -5030,13 +4964,13 @@ "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 haben und die Aufnahme in die Sammlung befürworten, wählen Sie \"Zustimmen\".", + "submission.workflow.tasks.claimed.approve_help": "Wenn Sie das Item begutachtet haben 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.edit_help": "Wählen Sie diese Option, um die Metadaten des Items 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 Einreichende:n das Problem beheben und die Ressource erneut einreichen kann.", @@ -5045,7 +4979,7 @@ "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.submit": "Item ablehnen", // "submission.workflow.tasks.claimed.reject.reason.title": "Reason", "submission.workflow.tasks.claimed.reject.reason.title": "Grund", @@ -5054,7 +4988,7 @@ "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 begutachtet 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.reject_help": "Wenn Sie das Item begutachtet 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 das Item erneut einzureichen.", // "submission.workflow.tasks.claimed.return": "Return to pool", "submission.workflow.tasks.claimed.return": "Zurück in den gemeinsamen Aufgabenbereich", @@ -5142,7 +5076,7 @@ "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", + "virtual-metadata.delete-relationship.modal-head": "Wählen Sie die Items für die Sie die virtuellen Metadaten als reelle Metadaten speichern wollen", From 07000297183c5182326132624cb0948f252dd708 Mon Sep 17 00:00:00 2001 From: Pascal-Nicolas Becker Date: Wed, 9 Mar 2022 16:28:38 +0100 Subject: [PATCH 071/435] Working on inclusivness of the German translation --- src/assets/i18n/de.json5 | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index d3fc1131aa..bbac4ccdef 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -107,7 +107,7 @@ "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 gekennzeichnet sind, dienen administrativen Zwecken und bleiben dem/der Endnutzer:in verborgen.", + "admin.registries.bitstream-formats.edit.internal.hint": "Dateiformate, die als intern gekennzeichnet sind, dienen administrativen Zwecken und bleiben dem:der Endnutzer:in verborgen.", // "admin.registries.bitstream-formats.edit.internal.label": "Internal", "admin.registries.bitstream-formats.edit.internal.label": "Intern", @@ -765,7 +765,7 @@ "bitstream.edit.form.description.label": "Beschreibung", // "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.hint": "Der erste Tag, ab dem der Zugang erlaubt ist. This date cannot be modified on this form. Um eine Embargo-Frist für eine Datei festzulegen, zu Item Status gehen, auf Authorizations... klicken, die Richtlinie READ für die Datei erstellen oder verändern und das Start Date wie gewünscht einstellen.", + "bitstream.edit.form.embargo.hint": "Der erste Tag, ab dem der Zugang erlaubt ist. Dieses Datum kann in diesem Formular nicht verändert werden. Um eine Embargo-Frist für eine Datei festzulegen, zu Item Status gehen, auf Authorizations... klicken, die Richtlinie READ für die Datei erstellen oder verändern und das Start Date wie gewünscht einstellen.", // "bitstream.edit.form.embargo.label": "Embargo until specific date", @@ -1499,7 +1499,7 @@ "cookies.consent.decline": "Ablehnen", // "cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: Authentication, Preferences, Acknowledgement and Statistics.
    To learn more, please read our {privacyPolicy}.", - "cookies.consent.content-notice.description": "Wir sammeln und verarbeiten Ihre personenbezogenen Daten für die folgenden Zwecke: Authentication, Preferences, Acknowledgement and Statistics.
    Um mehr zu erfahren, lesen Sie bitte unsere {privacyPolicy}.", + "cookies.consent.content-notice.description": "Wir sammeln und verarbeiten Ihre personenbezogenen Daten für die folgenden Zwecke: Authentifikation, Einstellungen, Zustimmungen und Statistiken.
    Um mehr zu erfahren, lesen Sie bitte unsere {privacyPolicy}.", // "cookies.consent.content-notice.learnMore": "Customize", "cookies.consent.content-notice.learnMore": "Anpassen", @@ -1598,7 +1598,7 @@ "curation.form.handle.label": "Handle:", // "curation.form.handle.hint": "Hint: Enter [your-handle-prefix]/0 to run a task across entire site (not all tasks may support this capability)", - "curation.form.handle.hint": "Hinweis: Geben Sie [your-handle-prefix]/0 ein, um eine Aufgabe für die gesamte Seite durchzuführen (nicht alle Aufgaben unterstützen diese Funktion)", + "curation.form.handle.hint": "Hinweis: Geben Sie [handle-prefix]/0 ein, um eine Aufgabe für die gesamte Seite durchzuführen (nicht alle Aufgaben unterstützen diese Funktion)", @@ -2636,7 +2636,7 @@ "item.page.link.simple": "Kurzanzeige", // "item.page.person.search.title": "Articles by this author", - "item.page.person.search.title": "Veröffentlichungen dieses/dieser Autor:in", + "item.page.person.search.title": "Veröffentlichungen dieses:dieser Autor:in", // "item.page.related-items.view-more": "Show {{ amount }} more", "item.page.related-items.view-more": "{{ amount }} mehr anzeigen", @@ -3182,7 +3182,7 @@ "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 das Item eingereicht hat, eine Nachricht zu schicken.", + "mydspace.messages.controller-help": "Wählen Sie diese Option, um dem:derjenigen, die das Item eingereicht hat, eine Nachricht zu schicken.", // "mydspace.messages.description-placeholder": "Insert your message here...", "mydspace.messages.description-placeholder": "Geben Sie Ihre Nachricht hier ein...", @@ -3290,10 +3290,10 @@ "mydspace.upload.upload-failed-moreonefile": "Unverarbeitbare Anfrage. Nur eine Datei ist erlaubt.", // "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", - "mydspace.upload.upload-multiple-successful": "{{qty}} neue(s) Arbeitsbereichitem(s) angelegt.", + "mydspace.upload.upload-multiple-successful": "{{qty}} neue(s) Arbeitsbereichsitem(s) angelegt.", // "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", - "mydspace.upload.upload-successful": "Neues Arbeitsbereichitem angelegt. Klicken Sie {{here}}, um sie zu bearbeiten.", + "mydspace.upload.upload-successful": "Neues Arbeitsbereichsitem angelegt. Klicken Sie {{here}}, um sie zu bearbeiten.", // "mydspace.view-btn": "View", "mydspace.view-btn": "Anzeige", @@ -3763,7 +3763,7 @@ "register-page.create-profile.submit.error.head": "Registrierung fehlgeschlagen.", // "register-page.create-profile.submit.success.content": "The registration was successful. You have been logged in as the created user.", - "register-page.create-profile.submit.success.content": "Die Registrierung war erfolgreich. Sie wurden als der/die angelegte Benutzer:in angemeldet.", + "register-page.create-profile.submit.success.content": "Die Registrierung war erfolgreich. Sie wurden als der:die angelegte Benutzer:in angemeldet.", // "register-page.create-profile.submit.success.head": "Registration completed", "register-page.create-profile.submit.success.head": "Registrierung abgeschlossen", @@ -4078,7 +4078,7 @@ "search.filters.filter.author.head": "Autor:in", // "search.filters.filter.author.placeholder": "Author name", - "search.filters.filter.author.placeholder": "Name des/der Autor:in", + "search.filters.filter.author.placeholder": "Name des:der Autor:in", // "search.filters.filter.birthDate.head": "Birth Date", "search.filters.filter.birthDate.head": "Geburtsdatum", @@ -4474,10 +4474,10 @@ "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.title": "Importiere Autor:innen-Metadaten", // "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Successfully added local author to the selection", - "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Erfolgreicher Import und Hinzufügen eines/einer lokalen Autor:in zur Auswahl", + "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.local-entity": "Erfolgreicher Import und Hinzufügen eines:einer lokalen Autor:in zur Auswahl", // "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.new-entity": "Successfully imported and added external author to the selection", - "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.new-entity": "Erfolgreicher Import und Hinzufügen eines/einer externen Autor:in zur Auswahl", + "submission.sections.describe.relationship-lookup.external-source.import-modal.isAuthorOfPublication.added.new-entity": "Erfolgreicher Import und Hinzufügen eines:einer externen Autor:in zur Auswahl", // "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Authority", "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Referenz", @@ -4573,7 +4573,7 @@ "submission.sections.describe.relationship-lookup.selected": "{{ size }} Items ausgewählt", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Local Authors ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Lokale Autoren ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Lokale Autor:innen ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Local Journals ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Lokale Zeitschriften ({{ count }})", @@ -4585,7 +4585,7 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.Publication": "Lokale Publicationen ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.Person": "Local Authors ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Person": "Lokale Autoren ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Person": "Lokale Autor:innen ({{ count }})", // "submission.sections.describe.relationship-lookup.search-tab.tab-title.OrgUnit": "Local Organizational Units ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.OrgUnit": "Lokale Organisationseinheiten ({{ count }})", @@ -4973,7 +4973,7 @@ "submission.workflow.tasks.claimed.edit_help": "Wählen Sie diese Option, um die Metadaten des Items 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 Einreichende:n das Problem beheben und die Ressource erneut einreichen kann.", + "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 Einreichende:n 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", @@ -4988,7 +4988,7 @@ "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 das Item begutachtet 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 das Item erneut einzureichen.", + "submission.workflow.tasks.claimed.reject_help": "Wenn Sie das Item begutachtet 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 das Item erneut einzureichen.", // "submission.workflow.tasks.claimed.return": "Return to pool", "submission.workflow.tasks.claimed.return": "Zurück in den gemeinsamen Aufgabenbereich", @@ -5111,22 +5111,22 @@ // "workflow-item.send-back.notification.success.title": "Sent back to submitter", - "workflow-item.send-back.notification.success.title": "An die/den Einreichende:n zurückgeschickt", + "workflow-item.send-back.notification.success.title": "An die:den Einreichende:n zurückgeschickt", // "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", - "workflow-item.send-back.notification.success.content": "Dieses Workflow-Item wurde erfolgreich an die/den Einreichende:n zurückgeschickt", + "workflow-item.send-back.notification.success.content": "Dieses Workflow-Item wurde erfolgreich an die:den Einreichende:n zurückgeschickt", // "workflow-item.send-back.notification.error.title": "Something went wrong", "workflow-item.send-back.notification.error.title": "Etwas ist schief gelaufen", // "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", - "workflow-item.send-back.notification.error.content": "Das Workflow-Item konnte nicht an die/den Einreichende:n zurückgeschickt werden", + "workflow-item.send-back.notification.error.content": "Das Workflow-Item konnte nicht an die:den Einreichende:n zurückgeschickt werden", // "workflow-item.send-back.title": "Send workflow item back to submitter", - "workflow-item.send-back.title": "Workflow-Item an die/den Einreichende:n zurücksenden", + "workflow-item.send-back.title": "Workflow-Item an die:den Einreichende:n zurücksenden", // "workflow-item.send-back.header": "Send workflow item back to submitter", - "workflow-item.send-back.header": "Workflow-Item an die/den Einreichende:n zurücksenden", + "workflow-item.send-back.header": "Workflow-Item an die:den Einreichende:n zurücksenden", // "workflow-item.send-back.button.cancel": "Cancel", "workflow-item.send-back.button.cancel": "Abbrechen", From 2ffb72320221b923d0064fec6fc47a04d34606ac Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 10 Mar 2022 13:04:22 +0530 Subject: [PATCH 072/435] [CST-5329] Add validate only check in the Import > Metadata page --- .../metadata-import-page.component.html | 4 +++ .../metadata-import-page.component.spec.ts | 25 ++++++++++++++++++- .../metadata-import-page.component.ts | 8 ++++++ src/assets/i18n/en.json5 | 2 ++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html index 42a04b0de6..c70bc45947 100644 --- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html +++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html @@ -1,6 +1,10 @@

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

    +

    + + {{'admin.metadata-import.page.validateOnly' | translate}} +

    { comp.setFile(fileMock); }); - describe('if proceed button is pressed', () => { + describe('if proceed button is pressed without validate only', () => { beforeEach(fakeAsync(() => { + comp.validateOnly = false; const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; proceed.click(); fixture.detectChanges(); @@ -107,6 +108,28 @@ describe('MetadataImportPageComponent', () => { }); }); + describe('if proceed button is pressed with validate only', () => { + beforeEach(fakeAsync(() => { + comp.validateOnly = true; + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('metadata-import script is invoked with -f fileName and the mockFile and -v validate-only', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }), + Object.assign(new ProcessParameter(), { name: '-v', value: true }), + ]; + expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); + }); + it('success notification is shown', () => { + expect(notificationService.success).toHaveBeenCalled(); + }); + it('redirected to process page', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45'); + }); + }); + describe('if proceed is pressed; but script invoke fails', () => { beforeEach(fakeAsync(() => { jasmine.getEnv().allowRespy(true); diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts index 3bdcca3084..deb16c0d73 100644 --- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts +++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts @@ -30,6 +30,11 @@ export class MetadataImportPageComponent { */ fileObject: File; + /** + * The validate only flag + */ + validateOnly = true; + public constructor(private location: Location, protected translate: TranslateService, protected notificationsService: NotificationsService, @@ -62,6 +67,9 @@ export class MetadataImportPageComponent { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }), ]; + if (this.validateOnly) { + parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true })); + } this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe( getFirstCompletedRemoteData(), diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index f33a195cfe..426fcb12d2 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -538,6 +538,8 @@ "admin.metadata-import.page.error.addFile": "Select file first!", + "admin.metadata-import.page.validateOnly": "Validate Only", + From 027b281d7a61031370a62f1388d23b8369410808 Mon Sep 17 00:00:00 2001 From: Sufiyan Shaikh Date: Thu, 10 Mar 2022 14:45:53 +0530 Subject: [PATCH 073/435] [CST-5329] Add validate only check in the Import > Metadata page --- .../metadata-import-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html index c70bc45947..fb96c4becd 100644 --- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html +++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html @@ -3,7 +3,7 @@

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

    - {{'admin.metadata-import.page.validateOnly' | translate}} + {{'admin.metadata-import.page.validateOnly' | translate}}

    Date: Fri, 11 Mar 2022 12:18:14 +0100 Subject: [PATCH 074/435] [CST-5449] Fix failed test --- .../shared/browse-by/browse-by.component.html | 6 ++--- .../browse-by/browse-by.component.spec.ts | 26 +++++++------------ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/app/shared/browse-by/browse-by.component.html b/src/app/shared/browse-by/browse-by.component.html index cc126768e4..3b72ce2d23 100644 --- a/src/app/shared/browse-by/browse-by.component.html +++ b/src/app/shared/browse-by/browse-by.component.html @@ -1,15 +1,15 @@ -

    {{title | translate}}

    +

    {{title | translate}}

    - - +
    diff --git a/src/app/shared/browse-by/browse-by.component.spec.ts b/src/app/shared/browse-by/browse-by.component.spec.ts index 1e739dafbf..601c352705 100644 --- a/src/app/shared/browse-by/browse-by.component.spec.ts +++ b/src/app/shared/browse-by/browse-by.component.spec.ts @@ -4,20 +4,17 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { of as observableOf } from 'rxjs'; -import { SharedModule } from '../shared.module'; import { CommonModule } from '@angular/common'; import { Item } from '../../core/shared/item.model'; import { buildPaginatedList } from '../../core/data/paginated-list.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { StoreModule } from '@ngrx/store'; import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; import { RouterTestingModule } from '@angular/router/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; -import { storeModuleConfig } from '../../app.reducer'; import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../testing/pagination-service.stub'; import { ListableObjectComponentLoaderComponent } from '../object-collection/shared/listable-object/listable-object-component-loader.component'; @@ -30,17 +27,18 @@ import { import { BrowseEntry } from '../../core/shared/browse-entry.model'; import { ITEM } from '../../core/shared/item.resource-type'; import { ThemeService } from '../theme-support/theme.service'; -import SpyObj = jasmine.SpyObj; import { SelectableListService } from '../object-list/selectable-list/selectable-list.service'; +import { HostWindowServiceStub } from '../testing/host-window-service.stub'; import { HostWindowService } from '../host-window.service'; -import { CSSVariableService } from '../sass-helper/sass-helper.service'; +import SpyObj = jasmine.SpyObj; @listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'custom') @Component({ selector: 'ds-browse-entry-list-element', template: '' }) -class MockThemedBrowseEntryListElementComponent {} +class MockThemedBrowseEntryListElementComponent { +} describe('BrowseByComponent', () => { let comp: BrowseByComponent; @@ -86,10 +84,7 @@ describe('BrowseByComponent', () => { TestBed.configureTestingModule({ imports: [ CommonModule, - TranslateModule.forRoot(), - SharedModule, NgbModule, - StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -97,16 +92,15 @@ describe('BrowseByComponent', () => { } }), RouterTestingModule, - BrowserAnimationsModule + NoopAnimationsModule ], declarations: [], providers: [ - {provide: PaginationService, useValue: paginationService}, - {provide: MockThemedBrowseEntryListElementComponent}, + { provide: PaginationService, useValue: paginationService }, + { provide: MockThemedBrowseEntryListElementComponent }, { provide: ThemeService, useValue: themeService }, - SelectableListService, - HostWindowService, - CSSVariableService + { provide: SelectableListService, useValue: {} }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); From d4ed4ca88350eccaab9f127644ff7843c7d61cfa Mon Sep 17 00:00:00 2001 From: reetagithub <51482276+reetagithub@users.noreply.github.com> Date: Mon, 14 Mar 2022 09:05:48 +0200 Subject: [PATCH 075/435] Corrected a typo --- src/assets/i18n/fi.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/fi.json5 b/src/assets/i18n/fi.json5 index 0348b906c2..02f020a45d 100644 --- a/src/assets/i18n/fi.json5 +++ b/src/assets/i18n/fi.json5 @@ -4120,7 +4120,7 @@ "search.filters.filter.dateSubmitted.placeholder": "Tallennnusajankohta", // "search.filters.filter.discoverable.head": "Private", - "search.filters.filter.discoverable.head": "Ykstyinen", + "search.filters.filter.discoverable.head": "Yksityinen", // "search.filters.filter.withdrawn.head": "Withdrawn", "search.filters.filter.withdrawn.head": "Poistettu käytöstä", From 3a4a10e45385b46ee977fd4282b86b8ed9b140f5 Mon Sep 17 00:00:00 2001 From: William Welling <8352733+wwelling@users.noreply.github.com> Date: Mon, 14 Mar 2022 08:26:22 -0500 Subject: [PATCH 076/435] set default env preboot to false leaving production env to true --- src/environments/environment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/environments/environment.ts b/src/environments/environment.ts index a64ccf2608..dc0e808be0 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -10,7 +10,7 @@ export const environment: Partial = { // Angular Universal settings universal: { - preboot: true, + preboot: false, async: true, time: false } From b21f76456d641192d1b9b65362c0c6a7396be5b1 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 14 Mar 2022 16:33:10 +0100 Subject: [PATCH 077/435] 88248: #346: Withdrawn item tombstone page --- .../full/full-item-page.component.html | 44 ++++++------ .../full/full-item-page.component.spec.ts | 67 ++++++++++++++++++- .../full/full-item-page.component.ts | 4 +- .../item-page/simple/item-page.component.html | 2 +- .../simple/item-page.component.spec.ts | 62 +++++++++++++++++ .../item-page/simple/item-page.component.ts | 12 +++- .../item-alerts/item-alerts.component.html | 17 +++-- 7 files changed, 178 insertions(+), 30 deletions(-) diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html index d2655c4ad0..e71dd92f96 100644 --- a/src/app/item-page/full/full-item-page.component.html +++ b/src/app/item-page/full/full-item-page.component.html @@ -4,19 +4,21 @@ -
    - -
    - +
    +
    + +
    + +
    -
    - - - + +
    + @@ -24,14 +26,16 @@ - -
    {{mdEntry.key}}{{mdValue.language}}
    - - - -
    -
    - + + + + + +
    +
    + +
    diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index b4ab926667..c7643a940e 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -11,12 +11,15 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../core/shared/item.model'; -import { of as observableOf } from 'rxjs'; +import { BehaviorSubject, of as observableOf } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { AuthService } from '../../core/auth/auth.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec'; +import { RemoteData } from '../../core/data/remote-data'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -30,6 +33,13 @@ const mockItem: Item = Object.assign(new Item(), { } }); +const mockWithdrawnItem: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: [], + relationships: createRelationshipsObservable(), + isWithdrawn: true +}); + const metadataServiceStub = { /* tslint:disable:no-empty */ processRemoteData: () => { @@ -44,6 +54,7 @@ describe('FullItemPageComponent', () => { let authService: AuthService; let routeStub: ActivatedRouteStub; let routeData; + let authorizationDataService: AuthorizationDataService; @@ -61,6 +72,10 @@ describe('FullItemPageComponent', () => { data: observableOf(routeData) }); + authorizationDataService = jasmine.createSpyObj('authorizationDataService', { + isAuthorized: observableOf(false), + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -74,6 +89,7 @@ describe('FullItemPageComponent', () => { { provide: ItemDataService, useValue: {} }, { provide: MetadataService, useValue: metadataServiceStub }, { provide: AuthService, useValue: authService }, + { provide: AuthorizationDataService, useValue: authorizationDataService }, ], schemas: [NO_ERRORS_SCHEMA] @@ -111,4 +127,53 @@ describe('FullItemPageComponent', () => { expect(simpleViewBtn).toBeFalsy(); }); })); + + describe('when the item is withdrawn and the user is an admin', () => { + beforeEach(() => { + comp.isAdmin$ = observableOf(true); + comp.itemRD$ = new BehaviorSubject>(createSuccessfulRemoteDataObject(mockWithdrawnItem)); + fixture.detectChanges(); + }); + + it('should display the item', () => { + const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); + expect(objectLoader.nativeElement).toBeDefined(); + }); + }); + describe('when the item is withdrawn and the user is not an admin', () => { + beforeEach(() => { + comp.itemRD$ = new BehaviorSubject>(createSuccessfulRemoteDataObject(mockWithdrawnItem)); + fixture.detectChanges(); + }); + + it('should not display the item', () => { + const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); + expect(objectLoader).toBeNull(); + }); + }); + + describe('when the item is not withdrawn and the user is an admin', () => { + beforeEach(() => { + comp.isAdmin$ = observableOf(true); + comp.itemRD$ = new BehaviorSubject>(createSuccessfulRemoteDataObject(mockItem)); + fixture.detectChanges(); + }); + + it('should display the item', () => { + const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); + expect(objectLoader.nativeElement).toBeDefined(); + }); + }); + + describe('when the item is not withdrawn and the user is not an admin', () => { + beforeEach(() => { + comp.itemRD$ = new BehaviorSubject>(createSuccessfulRemoteDataObject(mockItem)); + fixture.detectChanges(); + }); + + it('should display the item', () => { + const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); + expect(objectLoader.nativeElement).toBeDefined(); + }); + }); }); diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 7f1b6de614..369769c77d 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -15,6 +15,7 @@ import { fadeInOut } from '../../shared/animations/fade'; import { hasValue } from '../../shared/empty.util'; import { AuthService } from '../../core/auth/auth.service'; import { Location } from '@angular/common'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; /** @@ -46,8 +47,9 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, router: Router, items: ItemDataService, authService: AuthService, + authorizationService: AuthorizationDataService, private _location: Location) { - super(route, router, items, authService); + super(route, router, items, authService, authorizationService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/simple/item-page.component.html b/src/app/item-page/simple/item-page.component.html index e843155d10..3e834b53db 100644 --- a/src/app/item-page/simple/item-page.component.html +++ b/src/app/item-page/simple/item-page.component.html @@ -4,7 +4,7 @@ - +
    diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index ff5a1e38d5..2ad0bbb272 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -21,6 +21,7 @@ import { } from '../../shared/remote-data.utils'; import { AuthService } from '../../core/auth/auth.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -28,10 +29,18 @@ const mockItem: Item = Object.assign(new Item(), { relationships: createRelationshipsObservable() }); +const mockWithdrawnItem: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: [], + relationships: createRelationshipsObservable(), + isWithdrawn: true +}); + describe('ItemPageComponent', () => { let comp: ItemPageComponent; let fixture: ComponentFixture; let authService: AuthService; + let authorizationDataService: AuthorizationDataService; const mockMetadataService = { /* tslint:disable:no-empty */ @@ -48,6 +57,9 @@ describe('ItemPageComponent', () => { isAuthenticated: observableOf(true), setRedirectUrl: {} }); + authorizationDataService = jasmine.createSpyObj('authorizationDataService', { + isAuthorized: observableOf(false), + }); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ @@ -63,6 +75,7 @@ describe('ItemPageComponent', () => { { provide: MetadataService, useValue: mockMetadataService }, { provide: Router, useValue: {} }, { provide: AuthService, useValue: authService }, + { provide: AuthorizationDataService, useValue: authorizationDataService }, ], schemas: [NO_ERRORS_SCHEMA] @@ -102,4 +115,53 @@ describe('ItemPageComponent', () => { }); }); + describe('when the item is withdrawn and the user is an admin', () => { + beforeEach(() => { + comp.isAdmin$ = observableOf(true); + comp.itemRD$ = createSuccessfulRemoteDataObject$(mockWithdrawnItem); + fixture.detectChanges(); + }); + + it('should display the item', () => { + const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); + expect(objectLoader.nativeElement).toBeDefined(); + }); + }); + describe('when the item is withdrawn and the user is not an admin', () => { + beforeEach(() => { + comp.itemRD$ = createSuccessfulRemoteDataObject$(mockWithdrawnItem); + fixture.detectChanges(); + }); + + it('should not display the item', () => { + const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); + expect(objectLoader).toBeNull(); + }); + }); + + describe('when the item is not withdrawn and the user is an admin', () => { + beforeEach(() => { + comp.isAdmin$ = observableOf(true); + comp.itemRD$ = createSuccessfulRemoteDataObject$(mockItem); + fixture.detectChanges(); + }); + + it('should display the item', () => { + const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); + expect(objectLoader.nativeElement).toBeDefined(); + }); + }); + + describe('when the item is not withdrawn and the user is not an admin', () => { + beforeEach(() => { + comp.itemRD$ = createSuccessfulRemoteDataObject$(mockItem); + fixture.detectChanges(); + }); + + it('should display the item', () => { + const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); + expect(objectLoader.nativeElement).toBeDefined(); + }); + }); + }); diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index cc23ba86d5..0559dc2219 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,4 +1,4 @@ -import { map } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; @@ -13,6 +13,8 @@ import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shar import { ViewMode } from '../../core/shared/view-mode.model'; import { AuthService } from '../../core/auth/auth.service'; import { getItemPageRoute } from '../item-page-routing-paths'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; /** * This component renders a simple item page. @@ -48,11 +50,17 @@ export class ItemPageComponent implements OnInit { */ itemPageRoute$: Observable; + /** + * Whether the current user is an admin or not + */ + isAdmin$: Observable; + constructor( protected route: ActivatedRoute, private router: Router, private items: ItemDataService, private authService: AuthService, + private authorizationService: AuthorizationDataService ) { } /** @@ -67,5 +75,7 @@ export class ItemPageComponent implements OnInit { getAllSucceededRemoteDataPayload(), map((item) => getItemPageRoute(item)) ); + + this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf); } } diff --git a/src/app/shared/item/item-alerts/item-alerts.component.html b/src/app/shared/item/item-alerts/item-alerts.component.html index fce5c038e5..740ea2595d 100644 --- a/src/app/shared/item/item-alerts/item-alerts.component.html +++ b/src/app/shared/item/item-alerts/item-alerts.component.html @@ -1,8 +1,13 @@
    -
    - -
    -
    - -
    +
    + +
    +
    + +
    + {{'item.alerts.withdrawn' | translate}} + {{"404.link.home-page" | translate}} +
    +
    +
    From 90e469ee167a3c37046825f0aea0f98ec9b7064f Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 14 Mar 2022 17:21:34 +0100 Subject: [PATCH 078/435] Fix LGTM issue --- src/app/item-page/simple/item-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 0559dc2219..95fbd7a2e0 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,4 +1,4 @@ -import { map, switchMap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; From 97dc241c5208d949826a6ebd930615adb47c31c7 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 17 Mar 2022 13:00:00 +0100 Subject: [PATCH 079/435] 88300: Issue 1379 - Angular: Browse-by Subject does not allow to proceed to next pages once filtered --- .../shared/browse-by/browse-by.component.html | 23 +++- .../browse-by/browse-by.component.spec.ts | 20 +++- .../shared/browse-by/browse-by.component.ts | 19 ++- .../text/starts-with-text.component.html | 19 --- .../text/starts-with-text.component.spec.ts | 110 ------------------ src/app/shared/testing/route-service.stub.ts | 7 +- src/assets/i18n/en.json5 | 6 +- 7 files changed, 65 insertions(+), 139 deletions(-) diff --git a/src/app/shared/browse-by/browse-by.component.html b/src/app/shared/browse-by/browse-by.component.html index c133324681..4837c92db8 100644 --- a/src/app/shared/browse-by/browse-by.component.html +++ b/src/app/shared/browse-by/browse-by.component.html @@ -12,6 +12,10 @@
- - + +
- diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index 934f00b10c..84c61f569b 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -14,6 +14,7 @@ import { PaginationServiceStub } from '../testing/pagination-service.stub'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { FindListOptions } from '../../core/data/find-list-options.model'; +import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe'; describe('SearchFormComponent', () => { let comp: SearchFormComponent; @@ -37,7 +38,10 @@ describe('SearchFormComponent', () => { { provide: SearchConfigurationService, useValue: searchConfigService }, { provide: DSpaceObjectDataService, useValue: { findById: () => createSuccessfulRemoteDataObject$(undefined)} } ], - declarations: [SearchFormComponent] + declarations: [ + SearchFormComponent, + BrowserOnlyMockPipe, + ] }).compileComponents(); })); diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-filter.component.html index 230f072772..452433e165 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.html @@ -4,6 +4,7 @@ class="filter-name d-flex" [attr.aria-controls]="regionId" [id]="toggleId" [attr.aria-expanded]="false" [attr.aria-label]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate" + [attr.data-test]="'filter-toggle' | dsBrowserOnly" >
{{'search.filters.filter.' + filter.name + '.head'| translate}} diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts index 662b380ce8..7abe45ca8c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.spec.ts @@ -13,6 +13,7 @@ import { FilterType } from '../../models/filter-type.model'; import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub'; import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component'; import { SequenceService } from '../../../../core/shared/sequence.service'; +import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe'; describe('SearchFilterComponent', () => { let comp: SearchFilterComponent; @@ -62,7 +63,10 @@ describe('SearchFilterComponent', () => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule], - declarations: [SearchFilterComponent], + declarations: [ + SearchFilterComponent, + BrowserOnlyMockPipe, + ], providers: [ { provide: SearchService, useValue: searchServiceStub }, { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 37f4fafb1e..2388c2f13e 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -173,7 +173,7 @@ import { DsSelectComponent } from './ds-select/ds-select.component'; import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'; import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component'; - +import { BrowserOnlyPipe } from './utils/browser-only.pipe'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here CommonModule, @@ -215,7 +215,8 @@ const PIPES = [ ObjectKeysPipe, ObjectValuesPipe, ConsolePipe, - ObjNgFor + ObjNgFor, + BrowserOnlyPipe, ]; const COMPONENTS = [ diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.html b/src/app/shared/sidebar/filter/sidebar-filter.component.html index bd392aa715..79afaa7583 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.component.html +++ b/src/app/shared/sidebar/filter/sidebar-filter.component.html @@ -1,5 +1,5 @@
-
+
{{ label | translate }}
diff --git a/src/app/shared/testing/browser-only-mock.pipe.ts b/src/app/shared/testing/browser-only-mock.pipe.ts new file mode 100644 index 0000000000..12e5a7b2d7 --- /dev/null +++ b/src/app/shared/testing/browser-only-mock.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/** + * Support dsBrowserOnly in unit tests. + */ +@Pipe({ + name: 'dsBrowserOnly' +}) +export class BrowserOnlyMockPipe implements PipeTransform { + transform(value: string): string | undefined { + return value; + } +} diff --git a/src/app/shared/testing/test-module.ts b/src/app/shared/testing/test-module.ts index b43adde8ae..7d45dd41f3 100644 --- a/src/app/shared/testing/test-module.ts +++ b/src/app/shared/testing/test-module.ts @@ -5,6 +5,7 @@ import { SharedModule } from '../shared.module'; import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive.stub'; import { QueryParamsDirectiveStub } from './query-params-directive.stub'; import { RouterLinkDirectiveStub } from './router-link-directive.stub'; +import { BrowserOnlyMockPipe } from './browser-only-mock.pipe'; /** * This module isn't used. It serves to prevent the AoT compiler @@ -21,7 +22,8 @@ import { RouterLinkDirectiveStub } from './router-link-directive.stub'; QueryParamsDirectiveStub, MySimpleItemActionComponent, RouterLinkDirectiveStub, - NgComponentOutletDirectiveStub + NgComponentOutletDirectiveStub, + BrowserOnlyMockPipe, ], exports: [ QueryParamsDirectiveStub diff --git a/src/app/shared/utils/browser-only.pipe.ts b/src/app/shared/utils/browser-only.pipe.ts new file mode 100644 index 0000000000..fc95bfda22 --- /dev/null +++ b/src/app/shared/utils/browser-only.pipe.ts @@ -0,0 +1,24 @@ +import { Inject, Pipe, PipeTransform, PLATFORM_ID } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; + +/** + * A pipe that only returns its intput when run in the browser. + * Used to distinguish client-side rendered content from server-side rendered content. + */ +@Pipe({ + name: 'dsBrowserOnly' +}) +export class BrowserOnlyPipe implements PipeTransform { + constructor( + @Inject(PLATFORM_ID) private platformID: any, + ) { + } + + transform(value: string): string | undefined { + if (isPlatformBrowser((this.platformID))) { + return value; + } else { + return undefined; + } + } +} diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.html b/src/app/shared/view-mode-switch/view-mode-switch.component.html index 2863a4832b..5f70bc699c 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.html +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.html @@ -7,7 +7,7 @@ routerLinkActive="active" [class.active]="currentMode === viewModeEnum.ListElement" class="btn btn-secondary" - data-test="list-view"> + [attr.data-test]="'list-view' | dsBrowserOnly"> + [attr.data-test]="'grid-view' | dsBrowserOnly"> + [attr.data-test]="'detail-view' | dsBrowserOnly">
diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts index d361857413..5780ea5cf3 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts @@ -9,6 +9,7 @@ import { SearchService } from '../../core/shared/search/search.service'; import { ViewModeSwitchComponent } from './view-mode-switch.component'; import { SearchServiceStub } from '../testing/search-service.stub'; import { ViewMode } from '../../core/shared/view-mode.model'; +import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe'; @Component({ template: '' }) class DummyComponent { @@ -36,7 +37,8 @@ describe('ViewModeSwitchComponent', () => { ], declarations: [ ViewModeSwitchComponent, - DummyComponent + DummyComponent, + BrowserOnlyMockPipe, ], providers: [ { provide: SearchService, useValue: searchService }, diff --git a/src/app/submission/form/footer/submission-form-footer.component.html b/src/app/submission/form/footer/submission-form-footer.component.html index 7013c55667..e898e1c220 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.html +++ b/src/app/submission/form/footer/submission-form-footer.component.html @@ -3,6 +3,7 @@ +
+
+ + + +
+ +
+
+
+
+ +
- diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.scss b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.scss new file mode 100644 index 0000000000..c3694e6784 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.scss @@ -0,0 +1,4 @@ +.auth-bitstream-container { + margin-top: -1em; + margin-bottom: 1.5em; +} diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index 76597a135b..f3313d4cef 100644 --- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -1,3 +1,5 @@ +import { isEqual } from 'lodash'; +import { DSONameService } from './../../../core/breadcrumbs/dso-name.service'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @@ -25,7 +27,8 @@ interface BundleBitstreamsMapEntry { @Component({ selector: 'ds-item-authorizations', - templateUrl: './item-authorizations.component.html' + templateUrl: './item-authorizations.component.html', + styleUrls:['./item-authorizations.component.scss'] }) /** * Component that handles the item Authorizations @@ -36,14 +39,20 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * A map that contains all bitstream of the item's bundles * @type {Observable>>>} */ - public bundleBitstreamsMap: Map>> = new Map>>(); + public bundleBitstreamsMap: Map = new Map(); /** - * The list of bundle for the item + * The list of all bundles for the item * @type {Observable>} */ private bundles$: BehaviorSubject = new BehaviorSubject([]); + /** + * The list of bundles to be displayed in the template + * @type {BehaviorSubject} + */ + bundlesToShow$: BehaviorSubject = new BehaviorSubject([]); + /** * The target editing item * @type {Observable} @@ -56,6 +65,37 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { */ private subs: Subscription[] = []; + /** + * The size of the bundles to be loaded on demand + * @type {number} + */ + bunblesPerPage = 6; + + /** + * The number of current page + * @type {number} + */ + bunblesPageSize = 1; + + /** + * The flag to show or not the 'Load more' button + * based on the condition if all the bundles are loaded or not + * @type {boolean} + */ + allBundlesLoaded = false; + + /** + * Initial size of loaded bitstreams + * The size of incrementation used in bitstream pagination + */ + bitstreamSize = 4; + + /** + * The size of the loaded bitstremas at a certain moment + * @private + */ + private bitstreamPageSize = 4; + /** * Initialize instance variables * @@ -64,7 +104,8 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { */ constructor( private linkService: LinkService, - private route: ActivatedRoute + private route: ActivatedRoute, + private nameService: DSONameService ) { } @@ -72,16 +113,53 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * Initialize the component, setting up the bundle and bitstream within the item */ ngOnInit(): void { + this.getBundlesPerItem(); + } + + /** + * Return the item's UUID + */ + getItemUUID(): Observable { + return this.item$.pipe( + map((item: Item) => item.id), + first((UUID: string) => isNotEmpty(UUID)) + ); + } + + /** + * Return the item's name + */ + getItemName(): Observable { + return this.item$.pipe( + map((item: Item) => this.nameService.getName(item)) + ); + } + + /** + * Return all item's bundles + * + * @return an observable that emits all item's bundles + */ + getItemBundles(): Observable { + return this.bundles$.asObservable(); + } + + /** + * Get all bundles per item + * and all the bitstreams per bundle + * @param page number of current page + */ + getBundlesPerItem(page: number = 1) { this.item$ = this.route.data.pipe( map((data) => data.dso), getFirstSucceededRemoteDataWithNotEmptyPayload(), map((item: Item) => this.linkService.resolveLink( item, - followLink('bundles', {}, followLink('bitstreams')) + followLink('bundles', {findListOptions: {currentPage : page, elementsPerPage: this.bunblesPerPage}}, followLink('bitstreams')) )) ) as Observable; - const bundles$: Observable> = this.item$.pipe( + const bundles$: Observable> = this.item$.pipe( filter((item: Item) => isNotEmpty(item.bundles)), mergeMap((item: Item) => item.bundles), getFirstSucceededRemoteDataWithNotEmptyPayload(), @@ -96,37 +174,40 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { take(1), map((list: PaginatedList) => list.page) ).subscribe((bundles: Bundle[]) => { - this.bundles$.next(bundles); + if (isEqual(bundles.length,0) || bundles.length < this.bunblesPerPage) { + this.allBundlesLoaded = true; + } + if (isEqual(page, 1)) { + this.bundles$.next(bundles); + this.bundlesToShow$.next(bundles); + } else { + this.bundlesToShow$.next(this.bundles$.getValue().concat(bundles)); + this.bundles$.next(this.bundles$.getValue().concat(bundles)); + } }), bundles$.pipe( take(1), mergeMap((list: PaginatedList) => list.page), map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) })) ).subscribe((entry: BundleBitstreamsMapEntry) => { - this.bundleBitstreamsMap.set(entry.id, entry.bitstreams); + let bitstreamMapValues: BitstreamMapValue = { + isCollapsed: true, + allBitstreamsLoaded: false, + bitstreams: null + }; + let bits = entry.bitstreams.pipe( + map((b: PaginatedList) => { + bitstreamMapValues.allBitstreamsLoaded = b?.page.length < this.bitstreamSize ; + let firstLoaded = [...b.page.slice(0, this.bitstreamSize)]; + return firstLoaded; + }) + ); + bitstreamMapValues.bitstreams = bits; + this.bundleBitstreamsMap.set(entry.id, bitstreamMapValues); }) ); } - /** - * Return the item's UUID - */ - getItemUUID(): Observable { - return this.item$.pipe( - map((item: Item) => item.id), - first((UUID: string) => isNotEmpty(UUID)) - ); - } - - /** - * Return all item's bundles - * - * @return an observable that emits all item's bundles - */ - getItemBundles(): Observable { - return this.bundles$.asObservable(); - } - /** * Return all bundle's bitstreams * @@ -142,6 +223,48 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { ); } + /** + * Changes the collapsible state of the area that contains the bitstream list + * @param bundleId Id of bundle responsible for the requested bitstreams + */ + collapseArea(bundleId: string) { + this.bundleBitstreamsMap.get(bundleId).isCollapsed = !this.bundleBitstreamsMap.get(bundleId).isCollapsed; + } + + /** + * Loads as much bundles as initial value of bundleSize to be displayed + */ + onBunbleLoad(){ + this.bunblesPageSize ++; + this.getBundlesPerItem(this.bunblesPageSize); + } + + /** + * Calculates the bitstreams that are going to be loaded on demand, + * based on the number configured on this.bitstreamSize. + * @param bundle parent of bitstreams that are requested to be shown + * @returns Subscription + */ + onBitstreamsLoad(bundle: Bundle) { + return this.getBundleBitstreams(bundle).pipe( + map((res: PaginatedList) => { + let nextBitstreams = res?.page.slice(this.bitstreamPageSize, this.bitstreamPageSize + this.bitstreamSize); + let bitstreamsToShow = this.bundleBitstreamsMap.get(bundle.id).bitstreams.pipe( + map((existingBits: Bitstream[])=> { + return [... existingBits, ...nextBitstreams]; + }) + ); + this.bitstreamPageSize = this.bitstreamPageSize + this.bitstreamSize; + let bitstreamMapValues: BitstreamMapValue = { + bitstreams: bitstreamsToShow , + isCollapsed: this.bundleBitstreamsMap.get(bundle.id).isCollapsed, + allBitstreamsLoaded: res?.page.length <= this.bitstreamPageSize + }; + this.bundleBitstreamsMap.set(bundle.id, bitstreamMapValues); + }) + ).subscribe(); + } + /** * Unsubscribe from all subscriptions */ @@ -151,3 +274,9 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { .forEach((subscription) => subscription.unsubscribe()); } } + +export interface BitstreamMapValue { + bitstreams: Observable; + isCollapsed: boolean; + allBitstreamsLoaded: boolean; +} diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index 0a1ccf7952..5d11ac81f2 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -4,9 +4,16 @@
- {{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} {{resourceUUID}} + + {{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} + + {{resourceName}} + + ({{resourceUUID}}) + +
- -
@@ -21,13 +21,13 @@ [resourceName]="bitstream.name">
- +
- +
diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index f3313d4cef..a833b90b20 100644 --- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -45,13 +45,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * The list of all bundles for the item * @type {Observable>} */ - private bundles$: BehaviorSubject = new BehaviorSubject([]); - - /** - * The list of bundles to be displayed in the template - * @type {BehaviorSubject} - */ - bundlesToShow$: BehaviorSubject = new BehaviorSubject([]); + bundles$: BehaviorSubject = new BehaviorSubject([]); /** * The target editing item @@ -69,13 +63,13 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * The size of the bundles to be loaded on demand * @type {number} */ - bunblesPerPage = 6; + bunblesPerPage = 1; /** * The number of current page * @type {number} */ - bunblesPageSize = 1; + bunblesPageSize = 6; /** * The flag to show or not the 'Load more' button @@ -179,9 +173,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { } if (isEqual(page, 1)) { this.bundles$.next(bundles); - this.bundlesToShow$.next(bundles); } else { - this.bundlesToShow$.next(this.bundles$.getValue().concat(bundles)); this.bundles$.next(this.bundles$.getValue().concat(bundles)); } }), diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index 5d11ac81f2..d1fd9266b1 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -6,7 +6,6 @@
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} - {{resourceName}} ({{resourceUUID}}) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 76f4f538ea..cef52fde9c 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -848,6 +848,12 @@ "collection.edit.tabs.authorizations.title": "Collection Edit - Authorizations", + "collection.edit.item.authorizations.load-bundle-button": "Load more bundles", + + "collection.edit.item.authorizations.load-more-button": "Load more", + + "collection.edit.item.authorizations.show-bitstreams-button": "Show all Bitstreams' Policies for Bundle", + "collection.edit.tabs.metadata.head": "Edit Metadata", "collection.edit.tabs.metadata.title": "Collection Edit - Metadata", From 6cd41be3b80278fd086ffbf6ad7ad023a5aa3e67 Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Mon, 9 May 2022 12:01:46 +0200 Subject: [PATCH 314/435] [CST-5677] Changed bunblesPerPage value --- .../item-authorizations/item-authorizations.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index a833b90b20..f018d1c642 100644 --- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -63,7 +63,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * The size of the bundles to be loaded on demand * @type {number} */ - bunblesPerPage = 1; + bunblesPerPage = 6; /** * The number of current page From 32d7fca27af3d92eda08c998414a2927fb4ad2c3 Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Mon, 9 May 2022 12:02:50 +0200 Subject: [PATCH 315/435] [CST-5677] Changed bunblesPageSize value --- .../item-authorizations/item-authorizations.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index f018d1c642..3a7a7d95f2 100644 --- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -69,7 +69,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * The number of current page * @type {number} */ - bunblesPageSize = 6; + bunblesPageSize = 1; /** * The flag to show or not the 'Load more' button From 51058daf27f0d63aecdc3f28a981ddc23b108ef7 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 10 May 2022 09:25:59 +1200 Subject: [PATCH 316/435] [TLC-254] tidy imports in type bind service --- .../ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts index f8a7376c3e..5dd4a6627d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts @@ -15,7 +15,7 @@ import { OR_OPERATOR } from '@ng-dynamic-forms/core'; -import {hasNoValue, hasValue, isEmpty} from '../../../empty.util'; +import {hasNoValue, hasValue} from '../../../empty.util'; import { FormBuilderService } from '../form-builder.service'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants'; From 0b664431afb6306a67034e70261b406081b805f0 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 10 May 2022 18:10:50 +0200 Subject: [PATCH 317/435] [CST-5307] Create a standalone component for person claim button --- .../item-pages/person/person.component.html | 14 +- .../person/person.component.spec.ts | 52 +++- .../item-pages/person/person.component.ts | 81 +------ .../person-page-claim-button.component.html | 1 + .../person-page-claim-button.component.scss | 0 ...person-page-claim-button.component.spec.ts | 186 ++++++++++++++ .../person-page-claim-button.component.ts | 85 +++++++ src/app/shared/shared.module.ts | 229 +++++++++++++----- src/assets/i18n/en.json5 | 2 + 9 files changed, 501 insertions(+), 149 deletions(-) create mode 100644 src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.html create mode 100644 src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.scss create mode 100644 src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.spec.ts create mode 100644 src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.ts diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html index 7505a31327..96cfdbfacd 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -4,7 +4,7 @@
- +
@@ -20,18 +20,10 @@ [fields]="['person.email']" [label]="'person.page.email'"> - - - - - - - -
+ +
{{"item.page.link.full" | translate}} diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts b/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts index 93b3cf208d..efbc48a209 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts @@ -17,24 +17,12 @@ const mockItem: Item = Object.assign(new Item(), { value: 'fake@email.com' } ], - // 'person.identifier.orcid': [ - // { - // language: 'en_US', - // value: 'ORCID-1' - // } - // ], 'person.birthDate': [ { language: 'en_US', value: '1993' } ], - // 'person.identifier.staffid': [ - // { - // language: 'en_US', - // value: '1' - // } - // ], 'person.jobTitle': [ { language: 'en_US', @@ -62,4 +50,42 @@ const mockItem: Item = Object.assign(new Item(), { } }); -describe('PersonComponent', getItemPageFieldsTest(mockItem, PersonComponent)); +const mockItemWithTitle: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + metadata: { + 'person.email': [ + { + language: 'en_US', + value: 'fake@email.com' + } + ], + 'person.birthDate': [ + { + language: 'en_US', + value: '1993' + } + ], + 'person.jobTitle': [ + { + language: 'en_US', + value: 'Developer' + } + ], + 'dc.title': [ + { + language: 'en_US', + value: 'Doe, John' + } + ] + }, + relationships: createRelationshipsObservable(), + _links: { + self : { + href: 'item-href' + } + } +}); + +describe('PersonComponent with family and given names', getItemPageFieldsTest(mockItem, PersonComponent)); + +describe('PersonComponent with dc.title', getItemPageFieldsTest(mockItemWithTitle, PersonComponent)); diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts index 33db18681f..27fdd2ab15 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts @@ -1,20 +1,10 @@ -import {Component, OnInit} from '@angular/core'; +import { Component } from '@angular/core'; import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component'; import { ViewMode } from '../../../../core/shared/view-mode.model'; -import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; -import {MetadataValue} from '../../../../core/shared/metadata.models'; -import {FeatureID} from '../../../../core/data/feature-authorization/feature-id'; -import {mergeMap, take} from 'rxjs/operators'; -import {getFirstSucceededRemoteData} from '../../../../core/shared/operators'; -import {RemoteData} from '../../../../core/data/remote-data'; -import {ResearcherProfile} from '../../../../core/profile/model/researcher-profile.model'; -import {isNotUndefined} from '../../../../shared/empty.util'; -import {BehaviorSubject, Observable} from 'rxjs'; -import {RouteService} from '../../../../core/services/route.service'; -import {AuthorizationDataService} from '../../../../core/data/feature-authorization/authorization-data.service'; -import {ResearcherProfileService} from '../../../../core/profile/researcher-profile.service'; -import {NotificationsService} from '../../../../shared/notifications/notifications.service'; -import {TranslateService} from '@ngx-translate/core'; +import { + listableObjectComponent +} from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { MetadataValue } from '../../../../core/shared/metadata.models'; @listableObjectComponent('Person', ViewMode.StandalonePage) @Component({ @@ -25,76 +15,25 @@ import {TranslateService} from '@ngx-translate/core'; /** * The component for displaying metadata and relations of an item of the type Person */ -export class PersonComponent extends ItemComponent implements OnInit { - - claimable$: BehaviorSubject = new BehaviorSubject(false); - - constructor(protected routeService: RouteService, - protected authorizationService: AuthorizationDataService, - protected notificationsService: NotificationsService, - protected translate: TranslateService, - protected researcherProfileService: ResearcherProfileService) { - super(routeService); - } - - ngOnInit(): void { - super.ngOnInit(); - - this.authorizationService.isAuthorized(FeatureID.CanClaimItem, this.object._links.self.href).pipe( - take(1) - ).subscribe((isAuthorized: boolean) => { - this.claimable$.next(isAuthorized); - }); - - } - - /** - * Create a new researcher profile claiming the current item. - */ - claim() { - this.researcherProfileService.createFromExternalSource(this.object._links.self.href).pipe( - getFirstSucceededRemoteData(), - mergeMap((rd: RemoteData) => { - return this.researcherProfileService.findRelatedItemId(rd.payload); - })) - .subscribe((id: string) => { - if (isNotUndefined(id)) { - this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'), - this.translate.get('researcherprofile.success.claim.body')); - this.claimable$.next(false); - } else { - this.notificationsService.error( - this.translate.get('researcherprofile.error.claim.title'), - this.translate.get('researcherprofile.error.claim.body')); - } - }); - } - - /** - * Returns true if the item is claimable, false otherwise. - */ - isClaimable(): Observable { - return this.claimable$; - } +export class PersonComponent extends ItemComponent { /** * Returns the metadata values to be used for the page title. */ - getTitleMetadataValues(): MetadataValue[]{ + getTitleMetadataValues(): MetadataValue[] { const metadataValues = []; const familyName = this.object?.firstMetadata('person.familyName'); const givenName = this.object?.firstMetadata('person.givenName'); const title = this.object?.firstMetadata('dc.title'); - if (familyName){ + if (familyName) { metadataValues.push(familyName); } - if (givenName){ + if (givenName) { metadataValues.push(givenName); } - if (metadataValues.length === 0 && title){ + if (metadataValues.length === 0 && title) { metadataValues.push(title); } return metadataValues; } - } diff --git a/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.html b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.html new file mode 100644 index 0000000000..12d5d2b47d --- /dev/null +++ b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.html @@ -0,0 +1 @@ + diff --git a/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.scss b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.spec.ts b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.spec.ts new file mode 100644 index 0000000000..168517b47a --- /dev/null +++ b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.spec.ts @@ -0,0 +1,186 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { PersonPageClaimButtonComponent } from './person-page-claim-button.component'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; +import { TranslateLoaderMock } from '../../mocks/translate-loader.mock'; +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { RouteService } from '../../../core/services/route.service'; +import { routeServiceStub } from '../../testing/route-service.stub'; +import { Item } from '../../../core/shared/item.model'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; + +describe('PersonPageClaimButtonComponent', () => { + let scheduler: TestScheduler; + let component: PersonPageClaimButtonComponent; + let fixture: ComponentFixture; + + const mockItem: Item = Object.assign(new Item(), { + metadata: { + 'person.email': [ + { + language: 'en_US', + value: 'fake@email.com' + } + ], + 'person.birthDate': [ + { + language: 'en_US', + value: '1993' + } + ], + 'person.jobTitle': [ + { + language: 'en_US', + value: 'Developer' + } + ], + 'person.familyName': [ + { + language: 'en_US', + value: 'Doe' + } + ], + 'person.givenName': [ + { + language: 'en_US', + value: 'John' + } + ] + }, + _links: { + self: { + href: 'item-href' + } + } + }); + + const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: 'test-id', + visible: true, + type: 'profile', + _links: { + item: { + href: 'https://rest.api/rest/api/profiles/test-id/item' + }, + self: { + href: 'https://rest.api/rest/api/profiles/test-id' + }, + } + }); + + const notificationsService = new NotificationsServiceStub(); + + const authorizationDataService = jasmine.createSpyObj('authorizationDataService', { + isAuthorized: jasmine.createSpy('isAuthorized') + }); + + const researcherProfileService = jasmine.createSpyObj('researcherProfileService', { + createFromExternalSource: jasmine.createSpy('createFromExternalSource'), + findRelatedItemId: jasmine.createSpy('findRelatedItemId'), + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [PersonPageClaimButtonComponent], + providers: [ + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: ResearcherProfileService, useValue: researcherProfileService }, + { provide: RouteService, useValue: routeServiceStub }, + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PersonPageClaimButtonComponent); + component = fixture.componentInstance; + component.object = mockItem; + }); + + describe('when item can be claimed', () => { + beforeEach(() => { + authorizationDataService.isAuthorized.and.returnValue(observableOf(true)); + researcherProfileService.createFromExternalSource.calls.reset(); + researcherProfileService.findRelatedItemId.calls.reset(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create claim button', () => { + const btn = fixture.debugElement.query(By.css('[data-test="item-claim"]')); + expect(btn).toBeTruthy(); + }); + + describe('claim', () => { + describe('when successfully', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + researcherProfileService.createFromExternalSource.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + researcherProfileService.findRelatedItemId.and.returnValue(observableOf('test-id')); + }); + + it('should display success notification', () => { + scheduler.schedule(() => component.claim()); + scheduler.flush(); + + expect(researcherProfileService.findRelatedItemId).toHaveBeenCalled(); + expect(notificationsService.success).toHaveBeenCalled(); + }); + }); + + describe('when not successfully', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + researcherProfileService.createFromExternalSource.and.returnValue(createFailedRemoteDataObject$()); + }); + + it('should display success notification', () => { + scheduler.schedule(() => component.claim()); + scheduler.flush(); + + expect(researcherProfileService.findRelatedItemId).not.toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + }); + + }); + + describe('when item cannot be claimed', () => { + beforeEach(() => { + authorizationDataService.isAuthorized.and.returnValue(observableOf(false)); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create claim button', () => { + const btn = fixture.debugElement.query(By.css('[data-test="item-claim"]')); + expect(btn).toBeFalsy(); + }); + + }); +}); diff --git a/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.ts b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.ts new file mode 100644 index 0000000000..d2e8e888e1 --- /dev/null +++ b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.ts @@ -0,0 +1,85 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; +import { mergeMap, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + +import { RouteService } from '../../../core/services/route.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; +import { isNotEmpty } from '../../empty.util'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +@Component({ + selector: 'ds-person-page-claim-button', + templateUrl: './person-page-claim-button.component.html', + styleUrls: ['./person-page-claim-button.component.scss'] +}) +export class PersonPageClaimButtonComponent implements OnInit { + + /** + * The target person item to claim + */ + @Input() object: DSpaceObject; + + /** + * A boolean representing if item can be claimed or not + */ + claimable$: BehaviorSubject = new BehaviorSubject(false); + + constructor(protected routeService: RouteService, + protected authorizationService: AuthorizationDataService, + protected notificationsService: NotificationsService, + protected translate: TranslateService, + protected researcherProfileService: ResearcherProfileService) { + } + + ngOnInit(): void { + this.authorizationService.isAuthorized(FeatureID.CanClaimItem, this.object._links.self.href).pipe( + take(1) + ).subscribe((isAuthorized: boolean) => { + this.claimable$.next(isAuthorized); + }); + + } + + /** + * Create a new researcher profile claiming the current item. + */ + claim() { + this.researcherProfileService.createFromExternalSource(this.object._links.self.href).pipe( + getFirstCompletedRemoteData(), + mergeMap((rd: RemoteData) => { + console.log(rd); + if (rd.hasSucceeded) { + return this.researcherProfileService.findRelatedItemId(rd.payload); + } else { + return observableOf(null); + } + })) + .subscribe((id: string) => { + if (isNotEmpty(id)) { + this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'), + this.translate.get('researcherprofile.success.claim.body')); + this.claimable$.next(false); + } else { + this.notificationsService.error( + this.translate.get('researcherprofile.error.claim.title'), + this.translate.get('researcherprofile.error.claim.body')); + } + }); + } + + /** + * Returns true if the item is claimable, false otherwise. + */ + isClaimable(): Observable { + return this.claimable$; + } + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 847b3910e0..b2a36285bb 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -7,7 +7,12 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { NouisliderModule } from 'ng2-nouislider'; import { - NgbDatepickerModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbTimepickerModule, NgbTooltipModule, + NgbDatepickerModule, + NgbDropdownModule, + NgbNavModule, + NgbPaginationModule, + NgbTimepickerModule, + NgbTooltipModule, NgbTypeaheadModule, } from '@ng-bootstrap/ng-bootstrap'; import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core'; @@ -16,7 +21,9 @@ import { FileUploadModule } from 'ng2-file-upload'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MomentModule } from 'ngx-moment'; import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component'; -import { ExportMetadataSelectorComponent } from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; +import { + ExportMetadataSelectorComponent +} from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component'; import { ItemListElementComponent } from './object-list/item-list-element/item-types/item/item-list-element.component'; import { EnumKeysPipe } from './utils/enum-keys-pipe'; @@ -24,13 +31,21 @@ import { FileSizePipe } from './utils/file-size-pipe'; import { MetadataFieldValidator } from './utils/metadatafield-validator.directive'; import { SafeUrlPipe } from './utils/safe-url-pipe'; import { ConsolePipe } from './utils/console.pipe'; -import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component'; +import { + CollectionListElementComponent +} from './object-list/collection-list-element/collection-list-element.component'; import { CommunityListElementComponent } from './object-list/community-list-element/community-list-element.component'; -import { SearchResultListElementComponent } from './object-list/search-result-list-element/search-result-list-element.component'; +import { + SearchResultListElementComponent +} from './object-list/search-result-list-element/search-result-list-element.component'; import { ObjectListComponent } from './object-list/object-list.component'; -import { CollectionGridElementComponent } from './object-grid/collection-grid-element/collection-grid-element.component'; +import { + CollectionGridElementComponent +} from './object-grid/collection-grid-element/collection-grid-element.component'; import { CommunityGridElementComponent } from './object-grid/community-grid-element/community-grid-element.component'; -import { AbstractListableElementComponent } from './object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { + AbstractListableElementComponent +} from './object-collection/shared/object-collection-element/abstract-listable-element.component'; import { ObjectGridComponent } from './object-grid/object-grid.component'; import { ObjectCollectionComponent } from './object-collection/object-collection.component'; import { ErrorComponent } from './error/error.component'; @@ -38,7 +53,9 @@ import { LoadingComponent } from './loading/loading.component'; import { PaginationComponent } from './pagination/pagination.component'; import { ThumbnailComponent } from '../thumbnail/thumbnail.component'; import { SearchFormComponent } from './search-form/search-form.component'; -import { SearchResultGridElementComponent } from './object-grid/search-result-grid-element/search-result-grid-element.component'; +import { + SearchResultGridElementComponent +} from './object-grid/search-result-grid-element/search-result-grid-element.component'; import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component'; import { VarDirective } from './utils/var.directive'; import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component'; @@ -53,21 +70,33 @@ import { ChipsComponent } from './chips/chips.component'; import { NumberPickerComponent } from './number-picker/number-picker.component'; import { MockAdminGuard } from './mocks/admin-guard.service.mock'; import { AlertComponent } from './alert/alert.component'; -import { SearchResultDetailElementComponent } from './object-detail/my-dspace-result-detail-element/search-result-detail-element.component'; +import { + SearchResultDetailElementComponent +} from './object-detail/my-dspace-result-detail-element/search-result-detail-element.component'; import { ClaimedTaskActionsComponent } from './mydspace-actions/claimed-task/claimed-task-actions.component'; import { PoolTaskActionsComponent } from './mydspace-actions/pool-task/pool-task-actions.component'; import { ObjectDetailComponent } from './object-detail/object-detail.component'; -import { ItemDetailPreviewComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component'; -import { MyDSpaceItemStatusComponent } from './object-collection/shared/mydspace-item-status/my-dspace-item-status.component'; +import { + ItemDetailPreviewComponent +} from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component'; +import { + MyDSpaceItemStatusComponent +} from './object-collection/shared/mydspace-item-status/my-dspace-item-status.component'; import { WorkspaceitemActionsComponent } from './mydspace-actions/workspaceitem/workspaceitem-actions.component'; import { WorkflowitemActionsComponent } from './mydspace-actions/workflowitem/workflowitem-actions.component'; import { ItemSubmitterComponent } from './object-collection/shared/mydspace-item-submitter/item-submitter.component'; import { ItemActionsComponent } from './mydspace-actions/item/item-actions.component'; -import { ClaimedTaskActionsApproveComponent } from './mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component'; -import { ClaimedTaskActionsRejectComponent } from './mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component'; +import { + ClaimedTaskActionsApproveComponent +} from './mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component'; +import { + ClaimedTaskActionsRejectComponent +} from './mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component'; import { ObjNgFor } from './utils/object-ngfor.pipe'; import { BrowseByComponent } from './browse-by/browse-by.component'; -import { BrowseEntryListElementComponent } from './object-list/browse-entry-list-element/browse-entry-list-element.component'; +import { + BrowseEntryListElementComponent +} from './object-list/browse-entry-list-element/browse-entry-list-element.component'; import { DebounceDirective } from './utils/debounce.directive'; import { ClickOutsideDirective } from './utils/click-outside.directive'; import { EmphasizePipe } from './utils/emphasize.pipe'; @@ -76,53 +105,105 @@ import { CapitalizePipe } from './utils/capitalize.pipe'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; import { AuthorityConfidenceStateDirective } from './authority-confidence/authority-confidence-state.directive'; import { LangSwitchComponent } from './lang-switch/lang-switch.component'; -import { PlainTextMetadataListElementComponent } from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component'; -import { ItemMetadataListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component'; -import { MetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/metadata-representation-list-element.component'; +import { + PlainTextMetadataListElementComponent +} from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component'; +import { + ItemMetadataListElementComponent +} from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component'; +import { + MetadataRepresentationListElementComponent +} from './object-list/metadata-representation-list-element/metadata-representation-list-element.component'; import { ObjectValuesPipe } from './utils/object-values-pipe'; import { InListValidator } from './utils/in-list-validator.directive'; import { AutoFocusDirective } from './utils/auto-focus.directive'; import { StartsWithDateComponent } from './starts-with/date/starts-with-date.component'; import { StartsWithTextComponent } from './starts-with/text/starts-with-text.component'; import { DSOSelectorComponent } from './dso-selector/dso-selector/dso-selector.component'; -import { CreateCommunityParentSelectorComponent } from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; -import { CreateItemParentSelectorComponent } from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; -import { CreateCollectionParentSelectorComponent } from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; -import { CommunitySearchResultListElementComponent } from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; -import { CollectionSearchResultListElementComponent } from './object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component'; -import { EditItemSelectorComponent } from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; -import { EditCommunitySelectorComponent } from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; -import { EditCollectionSelectorComponent } from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; -import { ItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; -import { MetadataFieldWrapperComponent } from '../item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component'; +import { + CreateCommunityParentSelectorComponent +} from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; +import { + CreateItemParentSelectorComponent +} from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +import { + CreateCollectionParentSelectorComponent +} from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; +import { + CommunitySearchResultListElementComponent +} from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; +import { + CollectionSearchResultListElementComponent +} from './object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component'; +import { + EditItemSelectorComponent +} from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; +import { + EditCommunitySelectorComponent +} from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; +import { + EditCollectionSelectorComponent +} from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; +import { + ItemListPreviewComponent +} from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; +import { + MetadataFieldWrapperComponent +} from '../item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component'; import { MetadataValuesComponent } from '../item-page/field-components/metadata-values/metadata-values.component'; import { RoleDirective } from './roles/role.directive'; import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component'; -import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component'; -import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; -import { CollectionSearchResultGridElementComponent } from './object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; -import { CommunitySearchResultGridElementComponent } from './object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'; +import { + ClaimedTaskActionsReturnToPoolComponent +} from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component'; +import { + ItemDetailPreviewFieldComponent +} from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; +import { + CollectionSearchResultGridElementComponent +} from './object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; +import { + CommunitySearchResultGridElementComponent +} from './object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'; import { PageSizeSelectorComponent } from './page-size-selector/page-size-selector.component'; import { AbstractTrackableComponent } from './trackable/abstract-trackable.component'; -import { ComcolMetadataComponent } from './comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; +import { + ComcolMetadataComponent +} from './comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; import { ItemSelectComponent } from './object-select/item-select/item-select.component'; import { CollectionSelectComponent } from './object-select/collection-select/collection-select.component'; -import { FilterInputSuggestionsComponent } from './input-suggestions/filter-suggestions/filter-input-suggestions.component'; -import { DsoInputSuggestionsComponent } from './input-suggestions/dso-input-suggestions/dso-input-suggestions.component'; +import { + FilterInputSuggestionsComponent +} from './input-suggestions/filter-suggestions/filter-input-suggestions.component'; +import { + DsoInputSuggestionsComponent +} from './input-suggestions/dso-input-suggestions/dso-input-suggestions.component'; import { ItemGridElementComponent } from './object-grid/item-grid-element/item-types/item/item-grid-element.component'; import { TypeBadgeComponent } from './object-list/type-badge/type-badge.component'; -import { MetadataRepresentationLoaderComponent } from './metadata-representation/metadata-representation-loader.component'; +import { + MetadataRepresentationLoaderComponent +} from './metadata-representation/metadata-representation-loader.component'; import { MetadataRepresentationDirective } from './metadata-representation/metadata-representation.directive'; -import { ListableObjectComponentLoaderComponent } from './object-collection/shared/listable-object/listable-object-component-loader.component'; -import { ItemSearchResultListElementComponent } from './object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; +import { + ListableObjectComponentLoaderComponent +} from './object-collection/shared/listable-object/listable-object-component-loader.component'; +import { + ItemSearchResultListElementComponent +} from './object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; import { ListableObjectDirective } from './object-collection/shared/listable-object/listable-object.directive'; -import { ItemMetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; +import { + ItemMetadataRepresentationListElementComponent +} from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; import { PageWithSidebarComponent } from './sidebar/page-with-sidebar.component'; import { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component'; import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component'; import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component'; -import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; -import { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component'; +import { + SelectableListItemControlComponent +} from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; +import { + ImportableListItemControlComponent +} from './object-collection/shared/importable-list-item-control/importable-list-item-control.component'; import { ItemVersionsComponent } from './item/item-versions/item-versions.component'; import { SortablejsModule } from 'ngx-sortablejs'; import { LogInContainerComponent } from './log-in/container/log-in-container.component'; @@ -135,10 +216,16 @@ import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-ve import { FileValidator } from './utils/require-file.validator'; import { FileValueAccessorDirective } from './utils/file-value-accessor.directive'; import { FileSectionComponent } from '../item-page/simple/field-components/file-section/file-section.component'; -import { ModifyItemOverviewComponent } from '../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; -import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component'; +import { + ModifyItemOverviewComponent +} from '../item-page/edit-item-page/modify-item-overview/modify-item-overview.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'; +import { + ClaimedTaskActionsEditMetadataComponent +} from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; import { ImpersonateNavbarComponent } from './impersonate-navbar/impersonate-navbar.component'; import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive'; import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component'; @@ -146,34 +233,63 @@ import { CollectionDropdownComponent } from './collection-dropdown/collection-dr import { EntityDropdownComponent } from './entity-dropdown/entity-dropdown.component'; import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component'; import { CurationFormComponent } from '../curation-form/curation-form.component'; -import { PublicationSidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component'; -import { SidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/sidebar-search-list-element.component'; -import { CollectionSidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component'; -import { CommunitySidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component'; -import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component'; +import { + PublicationSidebarSearchListElementComponent +} from './object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component'; +import { + SidebarSearchListElementComponent +} from './object-list/sidebar-search-list-element/sidebar-search-list-element.component'; +import { + CollectionSidebarSearchListElementComponent +} from './object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component'; +import { + CommunitySidebarSearchListElementComponent +} from './object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component'; +import { + AuthorizedCollectionSelectorComponent +} from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component'; import { DsoPageEditButtonComponent } from './dso-page/dso-page-edit-button/dso-page-edit-button.component'; import { DsoPageVersionButtonComponent } from './dso-page/dso-page-version-button/dso-page-version-button.component'; import { HoverClassDirective } from './hover-class.directive'; -import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component'; +import { + ValidationSuggestionsComponent +} from './input-suggestions/validation-suggestions/validation-suggestions.component'; import { ItemAlertsComponent } from './item/item-alerts/item-alerts.component'; -import { ItemSearchResultGridElementComponent } from './object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; +import { + ItemSearchResultGridElementComponent +} from './object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component'; -import { GenericItemPageFieldComponent } from '../item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; -import { MetadataRepresentationListComponent } from '../item-page/simple/metadata-representation-list/metadata-representation-list.component'; +import { + GenericItemPageFieldComponent +} from '../item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; +import { + MetadataRepresentationListComponent +} from '../item-page/simple/metadata-representation-list/metadata-representation-list.component'; import { RelatedItemsComponent } from '../item-page/simple/related-items/related-items-component'; import { LinkMenuItemComponent } from './menu/menu-item/link-menu-item.component'; import { OnClickMenuItemComponent } from './menu/menu-item/onclick-menu-item.component'; import { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component'; import { SearchNavbarComponent } from '../search-navbar/search-navbar.component'; -import { ItemVersionsSummaryModalComponent } from './item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component'; -import { ItemVersionsDeleteModalComponent } from './item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component'; +import { + ItemVersionsSummaryModalComponent +} from './item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component'; +import { + ItemVersionsDeleteModalComponent +} from './item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component'; import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component'; -import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component'; +import { + BitstreamRequestACopyPageComponent +} from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component'; import { DsSelectComponent } from './ds-select/ds-select.component'; import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'; -import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; -import { ClaimItemSelectorComponent } from './dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component'; +import { + ThemedItemListPreviewComponent +} from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; +import { + ClaimItemSelectorComponent +} from './dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component'; import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component'; +import { PersonPageClaimButtonComponent } from './dso-page/person-page-claim-button/person-page-claim-button.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -410,6 +526,7 @@ const SHARED_ITEM_PAGE_COMPONENTS = [ MetadataValuesComponent, DsoPageEditButtonComponent, DsoPageVersionButtonComponent, + PersonPageClaimButtonComponent, ItemAlertsComponent, GenericItemPageFieldComponent, MetadataRepresentationListComponent, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index e4875d153e..868f79e490 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2762,6 +2762,8 @@ "person.page.lastname": "Last Name", + "person.page.name": "Name", + "person.page.link.full": "Show all metadata", "person.page.orcid": "ORCID", From 853dcecfb8e5cad295ae7f15ecfaf5433ae408f1 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 10 May 2022 18:14:00 +0200 Subject: [PATCH 318/435] [CST-5307] Refactoring and adding missing unit tests --- .../core/breadcrumbs/dso-name.service.spec.ts | 31 +- .../researcher-profile.service.spec.ts | 290 +++++++++++++++++ .../profile/researcher-profile.service.ts | 208 ++++++------ .../profile-claim.service.spec.ts | 215 ++++++++++++ .../profile-claim/profile-claim.service.ts | 59 ++-- ...ile-page-researcher-form.component.spec.ts | 7 +- .../profile-page-researcher-form.component.ts | 20 +- .../profile-page.component.spec.ts | 306 +++++++++++------- .../profile-page/profile-page.component.ts | 21 +- yarn.lock | 5 - 10 files changed, 876 insertions(+), 286 deletions(-) create mode 100644 src/app/core/profile/researcher-profile.service.spec.ts create mode 100644 src/app/profile-page/profile-claim/profile-claim.service.spec.ts diff --git a/src/app/core/breadcrumbs/dso-name.service.spec.ts b/src/app/core/breadcrumbs/dso-name.service.spec.ts index 7a399ce748..9f2f76599a 100644 --- a/src/app/core/breadcrumbs/dso-name.service.spec.ts +++ b/src/app/core/breadcrumbs/dso-name.service.spec.ts @@ -78,15 +78,32 @@ describe(`DSONameService`, () => { }); describe(`factories.Person`, () => { - beforeEach(() => { - spyOn(mockPerson, 'firstMetadataValue').and.returnValues(...mockPersonName.split(', ')); + describe(`with person.familyName and person.givenName`, () => { + beforeEach(() => { + spyOn(mockPerson, 'firstMetadataValue').and.returnValues(...mockPersonName.split(', ')); + }); + + it(`should return 'person.familyName, person.givenName'`, () => { + const result = (service as any).factories.Person(mockPerson); + expect(result).toBe(mockPersonName); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName'); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName'); + expect(mockPerson.firstMetadataValue).not.toHaveBeenCalledWith('dc.title'); + }); }); - it(`should return 'person.familyName, person.givenName'`, () => { - const result = (service as any).factories.Person(mockPerson); - expect(result).toBe(mockPersonName); - expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName'); - expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName'); + describe(`without person.familyName and person.givenName`, () => { + beforeEach(() => { + spyOn(mockPerson, 'firstMetadataValue').and.returnValues(undefined, undefined, mockPersonName); + }); + + it(`should return dc.title`, () => { + const result = (service as any).factories.Person(mockPerson); + expect(result).toBe(mockPersonName); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName'); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName'); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('dc.title'); + }); }); }); diff --git a/src/app/core/profile/researcher-profile.service.spec.ts b/src/app/core/profile/researcher-profile.service.spec.ts new file mode 100644 index 0000000000..103bae2719 --- /dev/null +++ b/src/app/core/profile/researcher-profile.service.spec.ts @@ -0,0 +1,290 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; + +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RequestService } from '../data/request.service'; +import { PageInfo } from '../shared/page-info.model'; +import { buildPaginatedList } from '../data/paginated-list.model'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { RestResponse } from '../cache/response.models'; +import { RequestEntry } from '../data/request-entry.model'; +import { ResearcherProfileService } from './researcher-profile.service'; +import { RouterMock } from '../../shared/mocks/router.mock'; +import { ResearcherProfile } from './model/researcher-profile.model'; +import { Item } from '../shared/item.model'; +import { ReplaceOperation } from 'fast-json-patch'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { PostRequest } from '../data/request.models'; + +describe('ResearcherProfileService', () => { + let scheduler: TestScheduler; + let service: ResearcherProfileService; + let serviceAsAny: any; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let responseCacheEntry: RequestEntry; + + const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241'; + const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241'; + const researcherProfileItem: Item = Object.assign(new Item(), { + id: itemId, + _links: { + self: { + href: `https://rest.api/rest/api/items/${itemId}` + }, + } + }); + const researcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: researcherProfileId, + visible: false, + type: 'profile', + _links: { + item: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item` + }, + self: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId}` + }, + } + }); + + const researcherProfilePatched: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: researcherProfileId, + visible: true, + type: 'profile', + _links: { + item: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item` + }, + self: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId}` + }, + } + }); + + const researcherProfileId2 = 'agbf9946-f4ce-479e-8f11-b90cbe9f7241'; + const anotherResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: researcherProfileId2, + visible: false, + type: 'profile', + _links: { + self: { + href: `https://rest.api/rest/api/profiles/${researcherProfileId2}` + }, + } + }); + const endpointURL = `https://rest.api/rest/api/profiles`; + const sourceUri = `https://rest.api/rest/api/external-source/profile`; + const requestURL = `https://rest.api/rest/api/profiles/${researcherProfileId}`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + const pageInfo = new PageInfo(); + const array = [researcherProfile, anotherResearcherProfile]; + const paginatedList = buildPaginatedList(pageInfo, array); + const researcherProfileRD = createSuccessfulRemoteDataObject(researcherProfile); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + + beforeEach(() => { + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: endpointURL }) + }); + + responseCacheEntry = new RequestEntry(); + responseCacheEntry.request = { href: 'https://rest.api/' } as any; + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + send: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + setStaleByHrefSubstring: jasmine.createSpy('setStaleByHrefSubstring') + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: hot('a|', { + a: researcherProfileRD + }), + buildList: hot('a|', { + a: paginatedListRD + }), + buildFromRequestUUID: hot('a|', { + a: researcherProfileRD + }) + }); + objectCache = {} as ObjectCacheService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const routerStub: any = new RouterMock(); + const itemService = jasmine.createSpyObj('ItemService', { + findByHref: jasmine.createSpy('findByHref') + }); + + service = new ResearcherProfileService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + http, + routerStub, + comparator, + itemService + ); + serviceAsAny = service; + + spyOn((service as any).dataService, 'create').and.callThrough(); + spyOn((service as any).dataService, 'delete').and.callThrough(); + spyOn((service as any).dataService, 'update').and.callThrough(); + spyOn((service as any).dataService, 'findById').and.callThrough(); + spyOn((service as any).dataService, 'findByHref').and.callThrough(); + spyOn((service as any).dataService, 'searchBy').and.callThrough(); + spyOn((service as any).dataService, 'getLinkPath').and.returnValue(observableOf(endpointURL)); + + }); + + describe('findById', () => { + it('should proxy the call to dataservice.findById with eperson UUID', () => { + scheduler.schedule(() => service.findById(researcherProfileId)); + scheduler.flush(); + + expect((service as any).dataService.findById).toHaveBeenCalledWith(researcherProfileId, true, true); + }); + + it('should return a ResearcherProfile object with the given id', () => { + const result = service.findById(researcherProfileId); + const expected = cold('a|', { + a: researcherProfileRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('create', () => { + it('should proxy the call to dataservice.create with eperson UUID', () => { + scheduler.schedule(() => service.create()); + scheduler.flush(); + + expect((service as any).dataService.create).toHaveBeenCalled(); + }); + + it('should return the RemoteData created', () => { + const result = service.create(); + const expected = cold('a|', { + a: researcherProfileRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('delete', () => { + it('should proxy the call to dataservice.delete', () => { + scheduler.schedule(() => service.delete(researcherProfile)); + scheduler.flush(); + + expect((service as any).dataService.delete).toHaveBeenCalledWith(researcherProfile.id); + }); + }); + + describe('findRelatedItemId', () => { + describe('with a related item', () => { + + beforeEach(() => { + (service as any).itemService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfileItem)); + }); + + it('should proxy the call to dataservice.findById with eperson UUID', () => { + scheduler.schedule(() => service.findRelatedItemId(researcherProfile)); + scheduler.flush(); + + expect((service as any).itemService.findByHref).toHaveBeenCalledWith(researcherProfile._links.item.href, false); + }); + + it('should return a ResearcherProfile object with the given id', () => { + const result = service.findRelatedItemId(researcherProfile); + const expected = cold('(a|)', { + a: itemId + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('without a related item', () => { + + beforeEach(() => { + (service as any).itemService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(null)); + }); + + it('should proxy the call to dataservice.findById with eperson UUID', () => { + scheduler.schedule(() => service.findRelatedItemId(researcherProfile)); + scheduler.flush(); + + expect((service as any).itemService.findByHref).toHaveBeenCalledWith(researcherProfile._links.item.href, false); + }); + + it('should return a ResearcherProfile object with the given id', () => { + const result = service.findRelatedItemId(researcherProfile); + const expected = cold('(a|)', { + a: undefined + }); + expect(result).toBeObservable(expected); + }); + }); + }); + + describe('setVisibility', () => { + let patchSpy; + beforeEach(() => { + spyOn((service as any), 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + }); + + it('should proxy the call to dataservice.patch', () => { + const replaceOperation: ReplaceOperation = { + path: '/visible', + op: 'replace', + value: true + }; + + scheduler.schedule(() => service.setVisibility(researcherProfile, true)); + scheduler.flush(); + + expect((service as any).patch).toHaveBeenCalledWith(researcherProfile, [replaceOperation]); + }); + }); + + describe('createFromExternalSource', () => { + let patchSpy; + beforeEach(() => { + spyOn((service as any), 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + }); + + it('should proxy the call to dataservice.patch', () => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const request = new PostRequest(requestUUID, endpointURL, sourceUri, options); + + scheduler.schedule(() => service.createFromExternalSource(sourceUri)); + scheduler.flush(); + + expect((service as any).requestService.send).toHaveBeenCalledWith(request); + expect((service as any).rdbService.buildFromRequestUUID).toHaveBeenCalledWith(requestUUID); + + }); + }); + +}); diff --git a/src/app/core/profile/researcher-profile.service.ts b/src/app/core/profile/researcher-profile.service.ts index 0220afb964..eab6ea3d48 100644 --- a/src/app/core/profile/researcher-profile.service.ts +++ b/src/app/core/profile/researcher-profile.service.ts @@ -2,27 +2,25 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; + import { Store } from '@ngrx/store'; -import { Operation, RemoveOperation, ReplaceOperation } from 'fast-json-patch'; -import { combineLatest, Observable, of as observableOf } from 'rxjs'; +import { Operation, ReplaceOperation } from 'fast-json-patch'; +import { Observable, of as observableOf } from 'rxjs'; import { catchError, find, map, switchMap, tap } from 'rxjs/operators'; -import { environment } from '../../../environments/environment'; + import { NotificationsService } from '../../shared/notifications/notifications.service'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { ConfigurationDataService } from '../data/configuration-data.service'; import { DataService } from '../data/data.service'; import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; import { ItemDataService } from '../data/item-data.service'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; -import { ConfigurationProperty } from '../shared/configuration-property.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Item } from '../shared/item.model'; import { NoContent } from '../shared/NoContent.model'; import { - getFinishedRemoteData, + getAllCompletedRemoteData, getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators'; @@ -31,25 +29,26 @@ import { RESEARCHER_PROFILE } from './model/researcher-profile.resource-type'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { PostRequest } from '../data/request.models'; import { hasValue } from '../../shared/empty.util'; -import {CoreState} from '../core-state.model'; +import { CoreState } from '../core-state.model'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * A private DataService implementation to delegate specific methods to. */ class ResearcherProfileServiceImpl extends DataService { - protected linkPath = 'profiles'; + protected linkPath = 'profiles'; - 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(); - } + 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(); + } } @@ -60,100 +59,102 @@ class ResearcherProfileServiceImpl extends DataService { @dataService(RESEARCHER_PROFILE) export class ResearcherProfileService { - dataService: ResearcherProfileServiceImpl; + dataService: ResearcherProfileServiceImpl; - responseMsToLive: number = 10 * 1000; + responseMsToLive: number = 10 * 1000; - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected router: Router, - protected comparator: DefaultChangeAnalyzer, - protected itemService: ItemDataService, - protected configurationService: ConfigurationDataService ) { + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected router: Router, + protected comparator: DefaultChangeAnalyzer, + protected itemService: ItemDataService) { - this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, store, objectCache, halService, - notificationsService, http, comparator); + this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, null, objectCache, halService, + notificationsService, http, comparator); - } + } - /** - * Find the researcher profile with the given uuid. - * - * @param uuid the profile uuid - */ - findById(uuid: string): Observable { - return this.dataService.findById(uuid, false) - .pipe ( getFinishedRemoteData(), - map((remoteData) => remoteData.payload)); - } + /** + * Find the researcher profile with the given uuid. + * + * @param uuid the profile uuid + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved + */ + public findById(uuid: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findById(uuid, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( + getAllCompletedRemoteData(), + ); + } - /** - * Create a new researcher profile for the current user. - */ - create(): Observable> { - return this.dataService.create( new ResearcherProfile()); - } + /** + * Create a new researcher profile for the current user. + */ + public create(): Observable> { + return this.dataService.create(new ResearcherProfile()); + } - /** - * Delete a researcher profile. - * - * @param researcherProfile the profile to delete - */ - delete(researcherProfile: ResearcherProfile): Observable { - return this.dataService.delete(researcherProfile.id).pipe( - getFirstCompletedRemoteData(), - tap((response: RemoteData) => { - if (response.isSuccess) { - this.requestService.setStaleByHrefSubstring(researcherProfile._links.self.href); - } + /** + * Delete a researcher profile. + * + * @param researcherProfile the profile to delete + */ + public delete(researcherProfile: ResearcherProfile): Observable { + return this.dataService.delete(researcherProfile.id).pipe( + getFirstCompletedRemoteData(), + tap((response: RemoteData) => { + if (response.isSuccess) { + this.requestService.setStaleByHrefSubstring(researcherProfile._links.self.href); + } + }), + map((response: RemoteData) => response.isSuccess) + ); + } + + /** + * Find the item id related to the given researcher profile. + * + * @param researcherProfile the profile to find for + */ + public findRelatedItemId(researcherProfile: ResearcherProfile): Observable { + return this.itemService.findByHref(researcherProfile._links.item.href, false) + .pipe(getFirstSucceededRemoteDataPayload(), + catchError((error) => { + console.debug(error); + return observableOf(null); }), - map((response: RemoteData) => response.isSuccess) + map((item) => item?.id) ); - } + } - /** - * Find the item id related to the given researcher profile. - * - * @param researcherProfile the profile to find for - */ - findRelatedItemId( researcherProfile: ResearcherProfile ): Observable { - return this.itemService.findByHref(researcherProfile._links.item.href, false) - .pipe (getFirstSucceededRemoteDataPayload(), - catchError((error) => { - console.debug(error); - return observableOf(null); - }), - map((item) => item != null ? item.id : null )); - } + /** + * Change the visibility of the given researcher profile setting the given value. + * + * @param researcherProfile the profile to update + * @param visible the visibility value to set + */ + public setVisibility(researcherProfile: ResearcherProfile, visible: boolean): Observable { - /** - * Change the visibility of the given researcher profile setting the given value. - * - * @param researcherProfile the profile to update - * @param visible the visibility value to set - */ - setVisibility(researcherProfile: ResearcherProfile, visible: boolean): Observable { + const replaceOperation: ReplaceOperation = { + path: '/visible', + op: 'replace', + value: visible + }; - const replaceOperation: ReplaceOperation = { - path: '/visible', - op: 'replace', - value: visible - }; - - return this.patch(researcherProfile, [replaceOperation]).pipe ( - switchMap( ( ) => this.findById(researcherProfile.id)) - ); - } - - patch(researcherProfile: ResearcherProfile, operations: Operation[]): Observable> { - return this.dataService.patch(researcherProfile, operations); - } + return this.patch(researcherProfile, [replaceOperation]).pipe( + switchMap(() => this.findById(researcherProfile.id)), + getFirstSucceededRemoteDataPayload() + ); + } /** * Creates a researcher profile starting from an external source URI @@ -179,4 +180,7 @@ export class ResearcherProfileService { return this.rdbService.buildFromRequestUUID(requestId); } + private patch(researcherProfile: ResearcherProfile, operations: Operation[]): Observable> { + return this.dataService.patch(researcherProfile, operations); + } } diff --git a/src/app/profile-page/profile-claim/profile-claim.service.spec.ts b/src/app/profile-page/profile-claim/profile-claim.service.spec.ts new file mode 100644 index 0000000000..4030c7900c --- /dev/null +++ b/src/app/profile-page/profile-claim/profile-claim.service.spec.ts @@ -0,0 +1,215 @@ +import { cold, getTestScheduler } from 'jasmine-marbles'; + +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { ProfileClaimService } from './profile-claim.service'; +import { SearchService } from '../../core/shared/search/search.service'; +import { ItemSearchResult } from '../../shared/object-collection/shared/item-search-result.model'; +import { SearchObjects } from '../../shared/search/models/search-objects.model'; +import { Item } from '../../core/shared/item.model'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { EPerson } from '../../core/eperson/models/eperson.model'; + +describe('ProfileClaimService', () => { + let scheduler: TestScheduler; + let service: ProfileClaimService; + let serviceAsAny: any; + let searchService: jasmine.SpyObj; + + const eperson: EPerson = Object.assign(new EPerson(), { + id: 'id', + metadata: { + 'eperson.firstname': [ + { + value: 'John' + } + ], + 'eperson.lastname': [ + { + value: 'Doe' + }, + ], + }, + email: 'fake@email.com' + }); + const item1: Item = Object.assign(new Item(), { + uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e', + metadata: { + 'person.email': [ + { + value: 'fake@email.com' + } + ], + 'person.familyName': [ + { + value: 'Doe' + } + ], + 'person.givenName': [ + { + value: 'John' + } + ] + }, + _links: { + self: { + href: 'item-href' + } + } + }); + const item2: Item = Object.assign(new Item(), { + uuid: 'c8279647-1acc-41ae-b036-951d5f65649b', + metadata: { + 'person.email': [ + { + value: 'fake2@email.com' + } + ], + 'dc.title': [ + { + value: 'John, Doe' + } + ] + }, + _links: { + self: { + href: 'item-href' + } + } + }); + const item3: Item = Object.assign(new Item(), { + uuid: 'c8279647-1acc-41ae-b036-951d5f65649b', + metadata: { + 'person.email': [ + { + value: 'fake3@email.com' + } + ], + 'dc.title': [ + { + value: 'John, Doe' + } + ] + }, + _links: { + self: { + href: 'item-href' + } + } + }); + + const searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 }); + const searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 }); + const searchResult3 = Object.assign(new ItemSearchResult(), { indexableObject: item3 }); + + const searchResult = Object.assign(new SearchObjects(), { + page: [searchResult1, searchResult2, searchResult3] + }); + const emptySearchResult = Object.assign(new SearchObjects(), { + page: [] + }); + const searchResultRD = createSuccessfulRemoteDataObject(searchResult); + const emptyRearchResultRD = createSuccessfulRemoteDataObject(emptySearchResult); + + beforeEach(() => { + scheduler = getTestScheduler(); + + searchService = jasmine.createSpyObj('SearchService', { + search: jasmine.createSpy('search') + }); + + service = new ProfileClaimService(searchService); + serviceAsAny = service; + }); + + describe('hasProfilesToSuggest', () => { + + describe('when has suggestions', () => { + beforeEach(() => { + spyOn(service, 'search').and.returnValue(observableOf(searchResultRD)); + }); + + it('should return true', () => { + const result = service.hasProfilesToSuggest(eperson); + const expected = cold('(a|)', { + a: true + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('when has not suggestions', () => { + beforeEach(() => { + spyOn(service, 'search').and.returnValue(observableOf(emptyRearchResultRD)); + }); + + it('should return false', () => { + const result = service.hasProfilesToSuggest(eperson); + const expected = cold('(a|)', { + a: false + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('when has not valid eperson', () => { + it('should return false', () => { + const result = service.hasProfilesToSuggest(null); + const expected = cold('(a|)', { + a: false + }); + expect(result).toBeObservable(expected); + }); + + }); + + }); + + describe('search', () => { + + describe('when has search results', () => { + beforeEach(() => { + searchService.search.and.returnValue(observableOf(searchResultRD)); + }); + + it('should return the proper search object', () => { + const result = service.search(eperson); + const expected = cold('(a|)', { + a: searchResultRD + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('when has not suggestions', () => { + beforeEach(() => { + searchService.search.and.returnValue(observableOf(emptyRearchResultRD)); + }); + + it('should return null', () => { + const result = service.search(eperson); + const expected = cold('(a|)', { + a: emptyRearchResultRD + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('when has not valid eperson', () => { + it('should return null', () => { + const result = service.search(null); + const expected = cold('(a|)', { + a: null + }); + expect(result).toBeObservable(expected); + }); + + }); + + }); +}); diff --git a/src/app/profile-page/profile-claim/profile-claim.service.ts b/src/app/profile-page/profile-claim/profile-claim.service.ts index 9ee2462778..a61404540b 100644 --- a/src/app/profile-page/profile-claim/profile-claim.service.ts +++ b/src/app/profile-page/profile-claim/profile-claim.service.ts @@ -1,16 +1,16 @@ import { Injectable } from '@angular/core'; + import { Observable, of } from 'rxjs'; -import { mergeMap, take } from 'rxjs/operators'; -import { ConfigurationDataService } from '../../core/data/configuration-data.service'; -import { PaginatedList } from '../../core/data/paginated-list.model'; +import { map } from 'rxjs/operators'; + import { RemoteData } from '../../core/data/remote-data'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { SearchService } from '../../core/shared/search/search.service'; -import { hasValue } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { SearchResult } from '../../shared/search/models/search-result.model'; -import { getFirstSucceededRemoteData } from './../../core/shared/operators'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { SearchObjects } from '../../shared/search/models/search-objects.model'; /** * Service that handle profiles claim. @@ -18,8 +18,7 @@ import { getFirstSucceededRemoteData } from './../../core/shared/operators'; @Injectable() export class ProfileClaimService { - constructor(private searchService: SearchService, - private configurationService: ConfigurationDataService) { + constructor(private searchService: SearchService) { } /** @@ -27,27 +26,21 @@ export class ProfileClaimService { * * @param eperson the eperson */ - canClaimProfiles(eperson: EPerson): Observable { - - const query = this.personQueryData(eperson); - - if (!hasValue(query) || query.length === 0) { - return of(false); - } - - return this.lookup(query).pipe( - mergeMap((rd: RemoteData>>) => of(rd.payload.totalElements > 0)) + hasProfilesToSuggest(eperson: EPerson): Observable { + return this.search(eperson).pipe( + map((rd: RemoteData>) => { + return isNotEmpty(rd) && rd.hasSucceeded && rd.payload?.page?.length > 0; + }) ); - } /** * Returns profiles that could be associated with the given user. * @param eperson the user */ - search(eperson: EPerson): Observable>>> { + search(eperson: EPerson): Observable>> { const query = this.personQueryData(eperson); - if (!hasValue(query) || query.length === 0) { + if (isEmpty(query)) { return of(null); } return this.lookup(query); @@ -57,21 +50,31 @@ export class ProfileClaimService { * Search object by the given query. * @param query the query for the search */ - private lookup(query: string): Observable>>> { - if (!hasValue(query)) { + private lookup(query: string): Observable>> { + if (isEmpty(query)) { return of(null); } return this.searchService.search(new PaginatedSearchOptions({ configuration: 'eperson_claims', query: query - })) - .pipe( - getFirstSucceededRemoteData(), - take(1)); + })).pipe( + getFirstCompletedRemoteData() + ); } + /** + * Return the search query for person lookup, from the given eperson + * + * @param eperson The eperson to use for the lookup + */ private personQueryData(eperson: EPerson): string { - return 'dc.title:' + eperson.name; + if (eperson) { + const firstname = eperson.firstMetadataValue('eperson.firstname'); + const lastname = eperson.firstMetadataValue('eperson.lastname'); + return 'dc.title:' + eperson.name + ' OR (person.familyName:' + lastname + ' AND person.givenName:' + firstname + ')'; + } else { + return null; + } } } diff --git a/src/app/profile-page/profile-page-researcher-form/profile-page-researcher-form.component.spec.ts b/src/app/profile-page/profile-page-researcher-form/profile-page-researcher-form.component.spec.ts index d12c445ce4..b928b20eef 100644 --- a/src/app/profile-page/profile-page-researcher-form/profile-page-researcher-form.component.spec.ts +++ b/src/app/profile-page/profile-page-researcher-form/profile-page-researcher-form.component.spec.ts @@ -16,6 +16,7 @@ import { ProfilePageResearcherFormComponent } from './profile-page-researcher-fo import { ProfileClaimService } from '../profile-claim/profile-claim.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { AuthService } from 'src/app/core/auth/auth.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; describe('ProfilePageResearcherFormComponent', () => { @@ -51,7 +52,7 @@ describe('ProfilePageResearcherFormComponent', () => { }); researcherProfileService = jasmine.createSpyObj('researcherProfileService', { - findById: observableOf(profile), + findById: createSuccessfulRemoteDataObject$(profile), create: observableOf(profile), setVisibility: observableOf(profile), delete: observableOf(true), @@ -61,7 +62,7 @@ describe('ProfilePageResearcherFormComponent', () => { notificationsServiceStub = new NotificationsServiceStub(); profileClaimService = jasmine.createSpyObj('profileClaimService', { - canClaimProfiles: observableOf(false), + hasProfilesToSuggest: observableOf(false), }); } @@ -91,7 +92,7 @@ describe('ProfilePageResearcherFormComponent', () => { }); it('should search the researcher profile for the current user', () => { - expect(researcherProfileService.findById).toHaveBeenCalledWith(user.id); + expect(researcherProfileService.findById).toHaveBeenCalledWith(user.id, false); }); describe('createProfile', () => { diff --git a/src/app/profile-page/profile-page-researcher-form/profile-page-researcher-form.component.ts b/src/app/profile-page/profile-page-researcher-form/profile-page-researcher-form.component.ts index 9bb3028ff4..a1887f8b31 100644 --- a/src/app/profile-page/profile-page-researcher-form/profile-page-researcher-form.component.ts +++ b/src/app/profile-page/profile-page-researcher-form/profile-page-researcher-form.component.ts @@ -4,17 +4,18 @@ import { Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable } from 'rxjs'; -import { filter, mergeMap, switchMap, take, tap } from 'rxjs/operators'; +import { mergeMap, switchMap, take, tap } from 'rxjs/operators'; -import { getFirstCompletedRemoteData } from '../../core/shared/operators'; -import { ClaimItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { + ClaimItemSelectorComponent +} from '../../shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { AuthService } from '../../core/auth/auth.service'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model'; import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; import { ProfileClaimService } from '../profile-claim/profile-claim.service'; -import { isNotEmpty } from '../../shared/empty.util'; @Component({ selector: 'ds-profile-page-researcher-form', @@ -77,10 +78,10 @@ export class ProfilePageResearcherFormComponent implements OnInit { this.processingCreate$.next(true); this.authService.getAuthenticatedUserFromStore().pipe( - switchMap((currentUser) => this.profileClaimService.canClaimProfiles(currentUser))) - .subscribe((canClaimProfiles) => { + switchMap((currentUser) => this.profileClaimService.hasProfilesToSuggest(currentUser))) + .subscribe((hasProfilesToSuggest) => { - if (canClaimProfiles) { + if (hasProfilesToSuggest) { this.processingCreate$.next(false); const modal = this.modalService.open(ClaimItemSelectorComponent); modal.componentInstance.dso = this.user; @@ -174,9 +175,8 @@ export class ProfilePageResearcherFormComponent implements OnInit { * Initializes the researcherProfile and researcherProfileItemId attributes using the profile of the current user. */ private initResearchProfile(): void { - this.researcherProfileService.findById(this.user.id).pipe( - take(1), - filter((researcherProfile) => isNotEmpty(researcherProfile)), + this.researcherProfileService.findById(this.user.id, false).pipe( + getFirstSucceededRemoteDataPayload(), tap((researcherProfile) => this.researcherProfile$.next(researcherProfile)), mergeMap((researcherProfile) => this.researcherProfileService.findRelatedItemId(researcherProfile)), ).subscribe((itemId: string) => { diff --git a/src/app/profile-page/profile-page.component.spec.ts b/src/app/profile-page/profile-page.component.spec.ts index 84aec0c0f1..fcbd4d9e4a 100644 --- a/src/app/profile-page/profile-page.component.spec.ts +++ b/src/app/profile-page/profile-page.component.spec.ts @@ -11,17 +11,17 @@ import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model'; import { EPersonDataService } from '../core/eperson/eperson-data.service'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { authReducer } from '../core/auth/auth.reducer'; -import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { createPaginatedList } from '../shared/testing/utils.test'; import { BehaviorSubject, of as observableOf } from 'rxjs'; import { AuthService } from '../core/auth/auth.service'; import { RestResponse } from '../core/cache/response.models'; import { provideMockStore } from '@ngrx/store/testing'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; -import { getTestScheduler } from 'jasmine-marbles'; +import { cold, getTestScheduler } from 'jasmine-marbles'; import { By } from '@angular/platform-browser'; -import {ConfigurationDataService} from '../core/data/configuration-data.service'; -import {ConfigurationProperty} from '../core/shared/configuration-property.model'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; describe('ProfilePageComponent', () => { let component: ProfilePageComponent; @@ -30,16 +30,28 @@ describe('ProfilePageComponent', () => { let initialState: any; let authService; + let authorizationService; let epersonService; let notificationsService; + let configurationService; const canChangePassword = new BehaviorSubject(true); + const validConfiguration = Object.assign(new ConfigurationProperty(), { + name: 'researcher-profile.entity-type', + values: [ + 'Person' + ] + }); + const emptyConfiguration = Object.assign(new ConfigurationProperty(), { + name: 'researcher-profile.entity-type', + values: [] + }); function init() { user = Object.assign(new EPerson(), { id: 'userId', groups: createSuccessfulRemoteDataObject$(createPaginatedList([])), - _links: {self: {href: 'test.com/uuid/1234567654321'}} + _links: { self: { href: 'test.com/uuid/1234567654321' } } }); initialState = { core: { @@ -54,7 +66,7 @@ describe('ProfilePageComponent', () => { } } }; - + authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword }); authService = jasmine.createSpyObj('authService', { getAuthenticatedUserFromStore: observableOf(user) }); @@ -67,6 +79,9 @@ describe('ProfilePageComponent', () => { error: {}, warning: {} }); + configurationService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: jasmine.createSpy('findByPropertyName') + }); } beforeEach(waitForAsync(() => { @@ -82,15 +97,8 @@ describe('ProfilePageComponent', () => { { provide: EPersonDataService, useValue: epersonService }, { provide: NotificationsService, useValue: notificationsService }, { provide: AuthService, useValue: authService }, - { provide: ConfigurationDataService, useValue: jasmine.createSpyObj('configurationDataService', { - findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { - name: 'researcher-profile.entity-type', - values: [ - 'Person' - ] - })) - })}, - { provide: AuthorizationDataService, useValue: jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword }) }, + { provide: ConfigurationDataService, useValue: configurationService }, + { provide: AuthorizationDataService, useValue: authorizationService }, provideMockStore({ initialState }), ], schemas: [NO_ERRORS_SCHEMA] @@ -100,148 +108,206 @@ describe('ProfilePageComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ProfilePageComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - describe('updateProfile', () => { - describe('when the metadata form returns false and the security form returns true', () => { - beforeEach(() => { - component.metadataForm = jasmine.createSpyObj('metadataForm', { - updateProfile: false + describe('', () => { + + beforeEach(() => { + configurationService.findByPropertyName.and.returnValue(createSuccessfulRemoteDataObject$(validConfiguration)); + fixture.detectChanges(); + }); + + describe('updateProfile', () => { + describe('when the metadata form returns false and the security form returns true', () => { + beforeEach(() => { + component.metadataForm = jasmine.createSpyObj('metadataForm', { + updateProfile: false + }); + spyOn(component, 'updateSecurity').and.returnValue(true); + component.updateProfile(); + }); + + it('should not display a warning', () => { + expect(notificationsService.warning).not.toHaveBeenCalled(); }); - spyOn(component, 'updateSecurity').and.returnValue(true); - component.updateProfile(); }); - it('should not display a warning', () => { - expect(notificationsService.warning).not.toHaveBeenCalled(); + describe('when the metadata form returns true and the security form returns false', () => { + beforeEach(() => { + component.metadataForm = jasmine.createSpyObj('metadataForm', { + updateProfile: true + }); + component.updateProfile(); + }); + + it('should not display a warning', () => { + expect(notificationsService.warning).not.toHaveBeenCalled(); + }); + }); + + describe('when the metadata form returns true and the security form returns true', () => { + beforeEach(() => { + component.metadataForm = jasmine.createSpyObj('metadataForm', { + updateProfile: true + }); + component.updateProfile(); + }); + + it('should not display a warning', () => { + expect(notificationsService.warning).not.toHaveBeenCalled(); + }); + }); + + describe('when the metadata form returns false and the security form returns false', () => { + beforeEach(() => { + component.metadataForm = jasmine.createSpyObj('metadataForm', { + updateProfile: false + }); + component.updateProfile(); + }); + + it('should display a warning', () => { + expect(notificationsService.warning).toHaveBeenCalled(); + }); }); }); - describe('when the metadata form returns true and the security form returns false', () => { - beforeEach(() => { - component.metadataForm = jasmine.createSpyObj('metadataForm', { - updateProfile: true + describe('updateSecurity', () => { + describe('when no password value present', () => { + let result; + + beforeEach(() => { + component.setPasswordValue(''); + + result = component.updateSecurity(); + }); + + it('should return false', () => { + expect(result).toEqual(false); + }); + + it('should not call epersonService.patch', () => { + expect(epersonService.patch).not.toHaveBeenCalled(); }); - component.updateProfile(); }); - it('should not display a warning', () => { - expect(notificationsService.warning).not.toHaveBeenCalled(); + describe('when password is filled in, but the password is invalid', () => { + let result; + + beforeEach(() => { + component.setPasswordValue('test'); + component.setInvalid(true); + result = component.updateSecurity(); + }); + + it('should return true', () => { + expect(result).toEqual(true); + expect(epersonService.patch).not.toHaveBeenCalled(); + }); + }); + + describe('when password is filled in, and is valid', () => { + let result; + let operations; + + beforeEach(() => { + component.setPasswordValue('testest'); + component.setInvalid(false); + + operations = [{ op: 'add', path: '/password', value: 'testest' }]; + result = component.updateSecurity(); + }); + + it('should return true', () => { + expect(result).toEqual(true); + }); + + it('should return call epersonService.patch', () => { + expect(epersonService.patch).toHaveBeenCalledWith(user, operations); + }); }); }); - describe('when the metadata form returns true and the security form returns true', () => { - beforeEach(() => { - component.metadataForm = jasmine.createSpyObj('metadataForm', { - updateProfile: true + describe('canChangePassword$', () => { + describe('when the user is allowed to change their password', () => { + beforeEach(() => { + canChangePassword.next(true); }); - component.updateProfile(); - }); - it('should not display a warning', () => { - expect(notificationsService.warning).not.toHaveBeenCalled(); - }); - }); - - describe('when the metadata form returns false and the security form returns false', () => { - beforeEach(() => { - component.metadataForm = jasmine.createSpyObj('metadataForm', { - updateProfile: false + it('should contain true', () => { + getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: true }); + }); + + it('should show the security section on the page', () => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('.security-section'))).not.toBeNull(); }); - component.updateProfile(); }); - it('should display a warning', () => { - expect(notificationsService.warning).toHaveBeenCalled(); + describe('when the user is not allowed to change their password', () => { + beforeEach(() => { + canChangePassword.next(false); + }); + + it('should contain false', () => { + getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: false }); + }); + + it('should not show the security section on the page', () => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('.security-section'))).toBeNull(); + }); }); }); }); - describe('updateSecurity', () => { - describe('when no password value present', () => { - let result; + describe('isResearcherProfileEnabled', () => { + + describe('when configuration service return values', () => { beforeEach(() => { - component.setPasswordValue(''); + configurationService.findByPropertyName.and.returnValue(createSuccessfulRemoteDataObject$(validConfiguration)); + fixture.detectChanges(); + }); - result = component.updateSecurity(); + it('should return true', () => { + const result = component.isResearcherProfileEnabled(); + const expected = cold('a', { + a: true + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('when configuration service return no values', () => { + + beforeEach(() => { + configurationService.findByPropertyName.and.returnValue(createSuccessfulRemoteDataObject$(emptyConfiguration)); + fixture.detectChanges(); }); it('should return false', () => { - expect(result).toEqual(false); - }); - - it('should not call epersonService.patch', () => { - expect(epersonService.patch).not.toHaveBeenCalled(); + const result = component.isResearcherProfileEnabled(); + const expected = cold('a', { + a: false + }); + expect(result).toBeObservable(expected); }); }); - describe('when password is filled in, but the password is invalid', () => { - let result; + describe('when configuration service return an error', () => { beforeEach(() => { - component.setPasswordValue('test'); - component.setInvalid(true); - result = component.updateSecurity(); - }); - - it('should return true', () => { - expect(result).toEqual(true); - expect(epersonService.patch).not.toHaveBeenCalled(); - }); - }); - - describe('when password is filled in, and is valid', () => { - let result; - let operations; - - beforeEach(() => { - component.setPasswordValue('testest'); - component.setInvalid(false); - - operations = [{ op: 'add', path: '/password', value: 'testest' }]; - result = component.updateSecurity(); - }); - - it('should return true', () => { - expect(result).toEqual(true); - }); - - it('should return call epersonService.patch', () => { - expect(epersonService.patch).toHaveBeenCalledWith(user, operations); - }); - }); - }); - - describe('canChangePassword$', () => { - describe('when the user is allowed to change their password', () => { - beforeEach(() => { - canChangePassword.next(true); - }); - - it('should contain true', () => { - getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: true }); - }); - - it('should show the security section on the page', () => { + configurationService.findByPropertyName.and.returnValue(createFailedRemoteDataObject$()); fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('.security-section'))).not.toBeNull(); - }); - }); - - describe('when the user is not allowed to change their password', () => { - beforeEach(() => { - canChangePassword.next(false); }); - it('should contain false', () => { - getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: false }); - }); - - it('should not show the security section on the page', () => { - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('.security-section'))).toBeNull(); + it('should return false', () => { + const result = component.isResearcherProfileEnabled(); + const expected = cold('a', { + a: false + }); + expect(result).toBeObservable(expected); }); }); }); diff --git a/src/app/profile-page/profile-page.component.ts b/src/app/profile-page/profile-page.component.ts index 9c22c8c950..374fa5220b 100644 --- a/src/app/profile-page/profile-page.component.ts +++ b/src/app/profile-page/profile-page.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import {BehaviorSubject, Observable} from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { EPerson } from '../core/eperson/models/eperson.model'; import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component'; import { NotificationsService } from '../shared/notifications/notifications.service'; @@ -9,18 +9,15 @@ import { RemoteData } from '../core/data/remote-data'; import { PaginatedList } from '../core/data/paginated-list.model'; import { filter, switchMap, tap } from 'rxjs/operators'; import { EPersonDataService } from '../core/eperson/eperson-data.service'; -import { - getAllSucceededRemoteData, - getRemoteDataPayload, - getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload -} from '../core/shared/operators'; +import { getAllSucceededRemoteData, getFirstCompletedRemoteData, getRemoteDataPayload } from '../core/shared/operators'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { followLink } from '../shared/utils/follow-link-config.model'; import { AuthService } from '../core/auth/auth.service'; import { Operation } from 'fast-json-patch'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; -import {ConfigurationDataService} from '../core/data/configuration-data.service'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; @Component({ selector: 'ds-profile-page', @@ -94,8 +91,10 @@ export class ProfilePageComponent implements OnInit { this.canChangePassword$ = this.user$.pipe(switchMap((user: EPerson) => this.authorizationService.isAuthorized(FeatureID.CanChangePassword, user._links.self.href))); this.configurationService.findByPropertyName('researcher-profile.entity-type').pipe( - getFirstSucceededRemoteDataPayload() - ).subscribe(() => this.isResearcherProfileEnabled$.next(true)); + getFirstCompletedRemoteData() + ).subscribe((configRD: RemoteData) => { + this.isResearcherProfileEnabled$.next(configRD.hasSucceeded && configRD.payload.values.length > 0); + }); } /** @@ -175,8 +174,8 @@ export class ProfilePageComponent implements OnInit { /** * Returns true if the researcher profile feature is enabled, false otherwise. */ - isResearcherProfileEnabled(){ - return this.isResearcherProfileEnabled$; + isResearcherProfileEnabled(): Observable { + return this.isResearcherProfileEnabled$.asObservable(); } } diff --git a/yarn.lock b/yarn.lock index c06853e625..65c1a61aba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8884,11 +8884,6 @@ ngx-ui-switch@^11.0.1: resolved "https://registry.yarnpkg.com/ngx-ui-switch/-/ngx-ui-switch-11.0.1.tgz#c7f1e97ebe698f827a26f49951b50492b22c7839" integrity sha512-N8QYT/wW+xJdyh/aeebTSLPA6Sgrwp69H6KAcW0XZueg/LF+FKiqyG6Po/gFHq2gDhLikwyJEMpny8sudTI08w== -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - nice-napi@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nice-napi/-/nice-napi-1.0.2.tgz#dc0ab5a1eac20ce548802fc5686eaa6bc654927b" From 1e9d393edf1e5ccb5c5849c1e4d9dfe4e8dfc5c6 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 11 May 2022 11:59:38 +0200 Subject: [PATCH 319/435] [CST-5307] Move profile-claim-item-modal.component to profile page module and add unit tests --- .../profile-claim-item-modal.component.html} | 4 +- ...profile-claim-item-modal.component.spec.ts | 223 ++++++++++++++++++ .../profile-claim-item-modal.component.ts | 105 +++++++++ src/app/profile-page/profile-page.module.ts | 6 +- .../claim-item-selector.component.spec.ts | 45 ---- .../claim-item-selector.component.ts | 69 ------ src/app/shared/shared.module.ts | 8 +- 7 files changed, 336 insertions(+), 124 deletions(-) rename src/app/{shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.html => profile-page/profile-claim-item-modal/profile-claim-item-modal.component.html} (93%) create mode 100644 src/app/profile-page/profile-claim-item-modal/profile-claim-item-modal.component.spec.ts create mode 100644 src/app/profile-page/profile-claim-item-modal/profile-claim-item-modal.component.ts delete mode 100644 src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.spec.ts delete mode 100644 src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.ts diff --git a/src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.html b/src/app/profile-page/profile-claim-item-modal/profile-claim-item-modal.component.html similarity index 93% rename from src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.html rename to src/app/profile-page/profile-claim-item-modal/profile-claim-item-modal.component.html index 9df49ba24b..eec9f437f1 100644 --- a/src/app/shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component.html +++ b/src/app/profile-page/profile-claim-item-modal/profile-claim-item-modal.component.html @@ -14,7 +14,7 @@
@@ -26,7 +26,7 @@

{{'researcher.profile.not.associated' | translate}}

- + diff --git a/src/app/shared/rss-feed/rss.component.html b/src/app/shared/rss-feed/rss.component.html new file mode 100644 index 0000000000..8868539b5c --- /dev/null +++ b/src/app/shared/rss-feed/rss.component.html @@ -0,0 +1,5 @@ + +
+ +
+ diff --git a/src/app/shared/rss-feed/rss.component.scss b/src/app/shared/rss-feed/rss.component.scss new file mode 100644 index 0000000000..929bb453ac --- /dev/null +++ b/src/app/shared/rss-feed/rss.component.scss @@ -0,0 +1,12 @@ +:host { + .dropdown-toggle::after { + display: none; + } + .dropdown-item { + padding-left: 20px; + } +} + +.margin-right { + margin-right: .5em; +} \ No newline at end of file diff --git a/src/app/shared/rss-feed/rss.component.spec.ts b/src/app/shared/rss-feed/rss.component.spec.ts new file mode 100644 index 0000000000..bbfd5442b3 --- /dev/null +++ b/src/app/shared/rss-feed/rss.component.spec.ts @@ -0,0 +1,109 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { RemoteData } from '../../core/data/remote-data'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; +import { Collection } from '../../core/shared/collection.model'; +import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { PaginatedSearchOptions } from '../search/paginated-search-options.model'; +import { PaginationServiceStub } from '../testing/pagination-service.stub'; +import { createPaginatedList } from '../testing/utils.test'; +import { RSSComponent } from './rss.component'; +import { of as observableOf } from 'rxjs'; + + + +describe('RssComponent', () => { + let comp: RSSComponent; + let options: SortOptions; + let fixture: ComponentFixture; + let uuid: string; + let query: string; + let groupDataService: GroupDataService; + let linkHeadService: LinkHeadService; + let configurationDataService: ConfigurationDataService; + let paginationService; + + beforeEach(waitForAsync(() => { + const mockCollection: Collection = Object.assign(new Collection(), { + id: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4', + name: 'test-collection', + _links: { + mappedItems: { + href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4/mappedItems' + }, + self: { + href: 'https://rest.api/collections/ce41d451-97ed-4a9c-94a1-7de34f16a9f4' + } + } + }); + configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); + linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '' + }); + const mockCollectionRD: RemoteData = createSuccessfulRemoteDataObject(mockCollection); + const mockSearchOptions = observableOf(new PaginatedSearchOptions({ + pagination: Object.assign(new PaginationComponentOptions(), { + id: 'search-page-configuration', + pageSize: 10, + currentPage: 1 + }), + sort: new SortOptions('dc.title', SortDirection.ASC), + scope: mockCollection.id + })); + groupDataService = jasmine.createSpyObj('groupsDataService', { + findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '' + }); + paginationService = new PaginationServiceStub(); + const searchConfigService = { + paginatedSearchOptions: mockSearchOptions + }; + TestBed.configureTestingModule({ + providers: [ + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: searchConfigService}, + { provide: PaginationService, useValue: paginationService } + ], + declarations: [RSSComponent] + }).compileComponents(); + })); + + beforeEach(() => { + options = new SortOptions('dc.title', SortDirection.DESC); + uuid = '2cfcf65e-0a51-4bcb-8592-b8db7b064790'; + query = 'test'; + fixture = TestBed.createComponent(RSSComponent); + comp = fixture.componentInstance; + }); + + it('should formulate the correct url given params in url', () => { + const route = comp.formulateRoute(uuid, options, query); + expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&sort=dc.title&sort_direction=DESC&query=test'); + }); + + it('should skip uuid if its null', () => { + const route = comp.formulateRoute(null, options, query); + expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=test'); + }); + + it('should default to query * if none provided', () => { + const route = comp.formulateRoute(null, options, null); + expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=*'); + }); +}); + diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts new file mode 100644 index 0000000000..036148b368 --- /dev/null +++ b/src/app/shared/rss-feed/rss.component.ts @@ -0,0 +1,94 @@ +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + ViewEncapsulation +} from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { environment } from '../../../../src/environments/environment'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { SortOptions } from '../../core/cache/models/sort-options.model'; +import { PaginationService } from '../../core/pagination/pagination.service'; + + +/** + * The default pagination controls component. + */ +@Component({ + exportAs: 'rssComponent', + selector: 'ds-rss', + styleUrls: ['rss.component.scss'], + templateUrl: 'rss.component.html', + changeDetection: ChangeDetectionStrategy.Default, + encapsulation: ViewEncapsulation.Emulated +}) +export class RSSComponent implements OnInit, OnDestroy { + + route$: BehaviorSubject; + + isEnabled$: BehaviorSubject = new BehaviorSubject(false); + + uuid: string; + configuration$: Observable; + sortOption$: Observable; + + constructor(private groupDataService: GroupDataService, + private linkHeadService: LinkHeadService, + private configurationService: ConfigurationDataService, + private searchConfigurationService: SearchConfigurationService, + protected paginationService: PaginationService) { + } + ngOnDestroy(): void { + this.linkHeadService.removeTag("rel='alternate'"); + } + + ngOnInit(): void { + this.configuration$ = this.searchConfigurationService.getCurrentConfiguration('default'); + + this.configurationService.findByPropertyName('websvc.opensearch.enable').pipe( + getFirstCompletedRemoteData(), + ).subscribe((result) => { + const enabled = Boolean(result.payload.values[0]); + this.isEnabled$.next(enabled); + }); + + this.searchConfigurationService.getCurrentQuery('').subscribe((query) => { + this.sortOption$ = this.paginationService.getCurrentSort(this.searchConfigurationService.paginationID, null, true); + this.sortOption$.subscribe((sort) => { + this.uuid = this.groupDataService.getUUIDFromString(window.location.href); + + const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, sort, query); + + this.linkHeadService.addTag({ + href: route, + type: 'application/atom+xml', + rel: 'alternate', + title: 'Sitewide Atom feed' + }); + this.route$ = new BehaviorSubject(route); + }); + }); + } + + formulateRoute(uuid: string, sort: SortOptions, query: string): string { + let route = 'search?format=atom'; + if (uuid) { + route += `&scope=${uuid}`; + } + if (sort.direction && sort.field) { + route += `&sort=${sort.field}&sort_direction=${sort.direction}`; + } + if (query) { + route += `&query=${query}`; + } else { + route += `&query=*`; + } + route = '/opensearch/' + route; + return route; + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 12b6a482dc..c70aff6192 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -173,10 +173,9 @@ import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-p import { DsSelectComponent } from './ds-select/ds-select.component'; import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'; import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; -import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component'; +import { RSSComponent } from './rss-feed/rss.component'; const MODULES = [ - // Do NOT include UniversalModule, HttpModule, or JsonpModule here CommonModule, SortablejsModule, FileUploadModule, @@ -239,6 +238,7 @@ const COMPONENTS = [ AbstractListableElementComponent, ObjectCollectionComponent, PaginationComponent, + RSSComponent, SearchFormComponent, PageWithSidebarComponent, SidebarDropdownComponent, diff --git a/src/environments/environment.dev.ts b/src/environments/environment.dev.ts new file mode 100644 index 0000000000..999abd32ee --- /dev/null +++ b/src/environments/environment.dev.ts @@ -0,0 +1,17 @@ +export const environment = { + ui: { + ssl: false, + host: 'localhost', + port: 18080, + nameSpace: '/' + }, + rest: { + ssl: false, + host: 'localhost', + port: 8080, + nameSpace: '/server' + }, + universal: { + preboot: false + } +}; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts new file mode 100644 index 0000000000..c31da7b791 --- /dev/null +++ b/src/environments/environment.prod.ts @@ -0,0 +1,17 @@ +export const environment = { + ui: { + ssl: false, + host: 'localhost', + port: 18080, + nameSpace: '/' + }, + rest: { + ssl: false, + host: 'localhost', + port: 8080, + nameSpace: '/server' + }, + universal: { + preboot: true + } +}; From b8a96e48a7b885cc4424b720bfb9dbf012e82f23 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Fri, 10 Dec 2021 12:25:51 -0500 Subject: [PATCH 324/435] Fix test files to pass down necessary providers --- .../collection-item-mapper.component.spec.ts | 28 ++++++++++++++++- ...page-sub-collection-list.component.spec.ts | 31 +++++++++++++++++++ ...-page-sub-community-list.component.spec.ts | 30 ++++++++++++++++++ src/app/core/services/link-head.service.ts | 8 +---- ...top-level-community-list.component.spec.ts | 30 ++++++++++++++++++ .../edit-relationship-list.component.spec.ts | 29 +++++++++++++++++ .../collection-select.component.spec.ts | 31 ++++++++++++++++++- .../item-select/item-select.component.spec.ts | 30 +++++++++++++++++- src/app/shared/rss-feed/rss.component.scss | 2 +- src/app/shared/rss-feed/rss.component.spec.ts | 7 +++-- .../search-configuration-service.stub.ts | 4 +++ 11 files changed, 216 insertions(+), 14 deletions(-) diff --git a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 7f0e6815ed..142604c9b2 100644 --- a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -43,6 +43,10 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; @@ -143,6 +147,25 @@ describe('CollectionItemMapperComponent', () => { isAuthorized: observableOf(true) }); + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '' + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], @@ -159,7 +182,10 @@ describe('CollectionItemMapperComponent', () => { { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, { provide: RouteService, useValue: routeServiceStub }, - { provide: AuthorizationDataService, useValue: authorizationDataService } + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, ] }).overrideComponent(CollectionItemMapperComponent, { set: { diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts index ec61fac613..c0ce5369ff 100644 --- a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts +++ b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -25,6 +25,14 @@ import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { ThemeService } from '../../shared/theme-support/theme.service'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { FindListOptions } from '../../core/data/find-list-options.model'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { SearchServiceStub } from '../../shared/testing/search-service.stub'; +import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub'; describe('CommunityPageSubCollectionList Component', () => { let comp: CommunityPageSubCollectionListComponent; @@ -122,6 +130,25 @@ describe('CommunityPageSubCollectionList Component', () => { themeService = getMockThemeService(); + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '' + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -138,6 +165,10 @@ describe('CommunityPageSubCollectionList Component', () => { { provide: PaginationService, useValue: paginationService }, { provide: SelectableListService, useValue: {} }, { provide: ThemeService, useValue: themeService }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index 2bc829a3b0..3392ada994 100644 --- a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -25,6 +25,13 @@ import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { ThemeService } from '../../shared/theme-support/theme.service'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { FindListOptions } from '../../core/data/find-list-options.model'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub'; +import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; +import { createPaginatedList } from '../../shared/testing/utils.test'; describe('CommunityPageSubCommunityListComponent Component', () => { let comp: CommunityPageSubCommunityListComponent; @@ -119,6 +126,25 @@ describe('CommunityPageSubCommunityListComponent Component', () => { } }; + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '' + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); + const paginationService = new PaginationServiceStub(); themeService = getMockThemeService(); @@ -139,6 +165,10 @@ describe('CommunityPageSubCommunityListComponent Component', () => { { provide: PaginationService, useValue: paginationService }, { provide: SelectableListService, useValue: {} }, { provide: ThemeService, useValue: themeService }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/core/services/link-head.service.ts b/src/app/core/services/link-head.service.ts index 29ab62ff13..12c3ff197d 100644 --- a/src/app/core/services/link-head.service.ts +++ b/src/app/core/services/link-head.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Optional, RendererFactory2, ViewEncapsulation, Inject } from '@angular/core'; +import { Injectable, RendererFactory2, ViewEncapsulation, Inject } from '@angular/core'; import { DOCUMENT } from '@angular/common'; @Injectable() @@ -22,7 +22,6 @@ export class LinkHeadService { const link = renderer.createElement('link'); const head = this.document.head; - const selector = this._parseSelector(tag); if (head === null) { throw new Error(' not found within DOCUMENT.'); @@ -61,11 +60,6 @@ export class LinkHeadService { } } } - - private _parseSelector(tag: LinkDefinition): string { - const attr: string = tag.rel ? 'rel' : 'hreflang'; - return `${attr}="${tag[attr]}"`; - } } export declare type LinkDefinition = { diff --git a/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts index eb52ca9243..2561770942 100644 --- a/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts +++ b/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts @@ -25,6 +25,13 @@ import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { ThemeService } from '../../shared/theme-support/theme.service'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { FindListOptions } from '../../core/data/find-list-options.model'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub'; describe('TopLevelCommunityList Component', () => { let comp: TopLevelCommunityListComponent; @@ -114,6 +121,25 @@ describe('TopLevelCommunityList Component', () => { themeService = getMockThemeService(); + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '' + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -130,6 +156,10 @@ describe('TopLevelCommunityList Component', () => { { provide: PaginationService, useValue: paginationService }, { provide: SelectableListService, useValue: {} }, { provide: ThemeService, useValue: themeService }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index b0d8046cf4..f9e889bba5 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -23,6 +23,12 @@ import { PaginationComponent } from '../../../../shared/pagination/pagination.co import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { RelationshipTypeService } from '../../../../core/data/relationship-type.service'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; +import { LinkHeadService } from '../../../../core/services/link-head.service'; +import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub'; +import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; let comp: EditRelationshipListComponent; let fixture: ComponentFixture; @@ -174,6 +180,25 @@ describe('EditRelationshipListComponent', () => { hostWindowService = new HostWindowServiceStub(1200); + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '' + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); + TestBed.configureTestingModule({ imports: [SharedModule, TranslateModule.forRoot()], declarations: [EditRelationshipListComponent], @@ -185,6 +210,10 @@ describe('EditRelationshipListComponent', () => { { provide: PaginationService, useValue: paginationService }, { provide: HostWindowService, useValue: hostWindowService }, { provide: RelationshipTypeService, useValue: relationshipTypeService }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, ], schemas: [ NO_ERRORS_SCHEMA ] diff --git a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts index 5714db2385..1b89fd32c1 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts +++ b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts @@ -17,6 +17,12 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../testing/pagination-service.stub'; import { of as observableOf } from 'rxjs'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { LinkHeadService } from '../../../core/services/link-head.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; describe('CollectionSelectComponent', () => { let comp: CollectionSelectComponent; @@ -44,6 +50,25 @@ describe('CollectionSelectComponent', () => { isAuthorized: observableOf(true) }); + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '' + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); + const paginationService = new PaginationServiceStub(); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -53,7 +78,11 @@ describe('CollectionSelectComponent', () => { { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub([mockCollectionList[1].id]) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: PaginationService, useValue: paginationService }, - { provide: AuthorizationDataService, useValue: authorizationDataService } + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/object-select/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts index de52f1c3c2..25340ee862 100644 --- a/src/app/shared/object-select/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -18,6 +18,12 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../testing/pagination-service.stub'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { LinkHeadService } from '../../../core/services/link-head.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; describe('ItemSelectComponent', () => { let comp: ItemSelectComponent; @@ -70,6 +76,24 @@ describe('ItemSelectComponent', () => { const authorizationDataService = new AuthorizationDataService(null, null, null, null, null, null, null, null, null, null); + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '' + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -79,7 +103,11 @@ describe('ItemSelectComponent', () => { { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub([mockItemList[1].id]) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: PaginationService, useValue: paginationService }, - { provide: AuthorizationDataService, useValue: authorizationDataService } + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/rss-feed/rss.component.scss b/src/app/shared/rss-feed/rss.component.scss index 929bb453ac..91310eddcb 100644 --- a/src/app/shared/rss-feed/rss.component.scss +++ b/src/app/shared/rss-feed/rss.component.scss @@ -9,4 +9,4 @@ .margin-right { margin-right: .5em; -} \ No newline at end of file +} diff --git a/src/app/shared/rss-feed/rss.component.spec.ts b/src/app/shared/rss-feed/rss.component.spec.ts index bbfd5442b3..cbdc2a2d0e 100644 --- a/src/app/shared/rss-feed/rss.component.spec.ts +++ b/src/app/shared/rss-feed/rss.component.spec.ts @@ -15,6 +15,7 @@ import { PaginationServiceStub } from '../testing/pagination-service.stub'; import { createPaginatedList } from '../testing/utils.test'; import { RSSComponent } from './rss.component'; import { of as observableOf } from 'rxjs'; +import { SearchConfigurationServiceStub } from '../testing/search-configuration-service.stub'; @@ -61,11 +62,11 @@ describe('RssComponent', () => { currentPage: 1 }), sort: new SortOptions('dc.title', SortDirection.ASC), - scope: mockCollection.id })); groupDataService = jasmine.createSpyObj('groupsDataService', { findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), - getGroupRegistryRouterLink: '' + getGroupRegistryRouterLink: '', + getUUIDFromString: '', }); paginationService = new PaginationServiceStub(); const searchConfigService = { @@ -76,7 +77,7 @@ describe('RssComponent', () => { { provide: GroupDataService, useValue: groupDataService }, { provide: LinkHeadService, useValue: linkHeadService }, { provide: ConfigurationDataService, useValue: configurationDataService }, - { provide: SearchConfigurationService, useValue: searchConfigService}, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, { provide: PaginationService, useValue: paginationService } ], declarations: [RSSComponent] diff --git a/src/app/shared/testing/search-configuration-service.stub.ts b/src/app/shared/testing/search-configuration-service.stub.ts index 80744ba59a..78b358f0d4 100644 --- a/src/app/shared/testing/search-configuration-service.stub.ts +++ b/src/app/shared/testing/search-configuration-service.stub.ts @@ -17,6 +17,10 @@ export class SearchConfigurationServiceStub { return observableOf('test-id'); } + getCurrentQuery(a) { + return observableOf(a); + } + getCurrentConfiguration(a) { return observableOf(a); } From b5943b48b47009a7f771db99530d57d5be2fd1fc Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Fri, 25 Feb 2022 12:14:16 -0500 Subject: [PATCH 325/435] w2p-86403 fix opensearch is disabled, router issues and links --- src/app/core/services/link-head.service.ts | 3 ++ src/app/shared/rss-feed/rss.component.ts | 32 +++++++++++++++++----- src/app/shared/shared.module.ts | 4 +++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/app/core/services/link-head.service.ts b/src/app/core/services/link-head.service.ts index 12c3ff197d..af5dddec8d 100644 --- a/src/app/core/services/link-head.service.ts +++ b/src/app/core/services/link-head.service.ts @@ -1,6 +1,9 @@ import { Injectable, RendererFactory2, ViewEncapsulation, Inject } from '@angular/core'; import { DOCUMENT } from '@angular/common'; +/** + * LinkHead Service injects tag into the head element during runtime. + */ @Injectable() export class LinkHeadService { constructor( diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 036148b368..11286e19e3 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -14,10 +14,11 @@ import { environment } from '../../../../src/environments/environment'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginationService } from '../../core/pagination/pagination.service'; +import { Router } from '@angular/router'; /** - * The default pagination controls component. + * The Rss feed button componenet. */ @Component({ exportAs: 'rssComponent', @@ -41,6 +42,7 @@ export class RSSComponent implements OnInit, OnDestroy { private linkHeadService: LinkHeadService, private configurationService: ConfigurationDataService, private searchConfigurationService: SearchConfigurationService, + private router: Router, protected paginationService: PaginationService) { } ngOnDestroy(): void { @@ -53,22 +55,22 @@ export class RSSComponent implements OnInit, OnDestroy { this.configurationService.findByPropertyName('websvc.opensearch.enable').pipe( getFirstCompletedRemoteData(), ).subscribe((result) => { - const enabled = Boolean(result.payload.values[0]); + const enabled = (result.payload.values[0] === 'true'); this.isEnabled$.next(enabled); }); this.searchConfigurationService.getCurrentQuery('').subscribe((query) => { this.sortOption$ = this.paginationService.getCurrentSort(this.searchConfigurationService.paginationID, null, true); this.sortOption$.subscribe((sort) => { - this.uuid = this.groupDataService.getUUIDFromString(window.location.href); + this.uuid = this.groupDataService.getUUIDFromString(this.router.url); const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, sort, query); - + this.addLinks(route); this.linkHeadService.addTag({ - href: route, + href: environment.rest.baseUrl + '/opensearch/service', type: 'application/atom+xml', - rel: 'alternate', - title: 'Sitewide Atom feed' + rel: 'search', + title: 'Dspace' }); this.route$ = new BehaviorSubject(route); }); @@ -91,4 +93,20 @@ export class RSSComponent implements OnInit, OnDestroy { route = '/opensearch/' + route; return route; } + + addLinks(route: string): void { + this.linkHeadService.addTag({ + href: route, + type: 'application/atom+xml', + rel: 'alternate', + title: 'Sitewide Atom feed' + }); + route = route.replace('format=atom', 'format=rss'); + this.linkHeadService.addTag({ + href: route, + type: 'application/rss+xml', + rel: 'alternate', + title: 'Sitewide RSS feed' + }); + } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c70aff6192..c6cc3d2bd9 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -174,6 +174,10 @@ import { DsSelectComponent } from './ds-select/ds-select.component'; import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'; import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; import { RSSComponent } from './rss-feed/rss.component'; +import { SearchObjects } from './search/models/search-objects.model'; +import { FacetConfigResponse } from './search/models/facet-config-response.model'; +import { SearchResult } from './search/models/search-result.model'; +import { FacetValues } from './search/models/facet-values.model'; const MODULES = [ CommonModule, From efb76ea88328df3d03706ad3c5f0411dcf7077a9 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Fri, 25 Feb 2022 12:35:17 -0500 Subject: [PATCH 326/435] Fix tests now that router is apart of the rss component --- .../edit-relationship-list.component.spec.ts | 3 +++ src/app/shared/rss-feed/rss.component.spec.ts | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index f9e889bba5..2403d8f443 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -29,6 +29,8 @@ import { LinkHeadService } from '../../../../core/services/link-head.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub'; import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; +import { Router } from '@angular/router'; +import { RouterMock } from '../../../../shared/mocks/router.mock'; let comp: EditRelationshipListComponent; let fixture: ComponentFixture; @@ -211,6 +213,7 @@ describe('EditRelationshipListComponent', () => { { provide: HostWindowService, useValue: hostWindowService }, { provide: RelationshipTypeService, useValue: relationshipTypeService }, { provide: GroupDataService, useValue: groupDataService }, + { provide: Router, useValue: new RouterMock() }, { provide: LinkHeadService, useValue: linkHeadService }, { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, diff --git a/src/app/shared/rss-feed/rss.component.spec.ts b/src/app/shared/rss-feed/rss.component.spec.ts index cbdc2a2d0e..b304abda83 100644 --- a/src/app/shared/rss-feed/rss.component.spec.ts +++ b/src/app/shared/rss-feed/rss.component.spec.ts @@ -10,12 +10,14 @@ import { ConfigurationProperty } from '../../core/shared/configuration-property. import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; -import { PaginatedSearchOptions } from '../search/paginated-search-options.model'; import { PaginationServiceStub } from '../testing/pagination-service.stub'; import { createPaginatedList } from '../testing/utils.test'; import { RSSComponent } from './rss.component'; import { of as observableOf } from 'rxjs'; import { SearchConfigurationServiceStub } from '../testing/search-configuration-service.stub'; +import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; +import { Router } from '@angular/router'; +import { RouterMock } from '../mocks/router.mock'; @@ -78,7 +80,8 @@ describe('RssComponent', () => { { provide: LinkHeadService, useValue: linkHeadService }, { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, - { provide: PaginationService, useValue: paginationService } + { provide: PaginationService, useValue: paginationService }, + { provide: Router, useValue: new RouterMock() } ], declarations: [RSSComponent] }).compileComponents(); From ffb34da3e74ce1354f3a29559adf36b856947344 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Fri, 25 Feb 2022 14:56:36 -0500 Subject: [PATCH 327/435] Create link head spec file --- .../core/services/link-head.service.spec.ts | 45 +++++++++++++++++++ src/app/core/services/link-head.service.ts | 17 +++++-- 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/app/core/services/link-head.service.spec.ts diff --git a/src/app/core/services/link-head.service.spec.ts b/src/app/core/services/link-head.service.spec.ts new file mode 100644 index 0000000000..017fe6af03 --- /dev/null +++ b/src/app/core/services/link-head.service.spec.ts @@ -0,0 +1,45 @@ +import { DOCUMENT } from '@angular/common'; +import { Renderer2, RendererFactory2 } from '@angular/core'; +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { MockProvider } from 'ng-mocks'; +import { LinkHeadService } from './link-head.service'; + +describe('LinkHeadService', () => { + + let service: LinkHeadService; + + const renderer2: Renderer2 = { + createRenderer: jasmine.createSpy('createRenderer'), + createElement: jasmine.createSpy('createElement'), + setAttribute: jasmine.createSpy('setAttribute'), + appendChild: jasmine.createSpy('appendChild') + } as unknown as Renderer2; + + beforeEach(waitForAsync(() => { + return TestBed.configureTestingModule({ + providers: [ + MockProvider(RendererFactory2, { + createRenderer: () => renderer2 + }), + { provide: Document, useExisting: DOCUMENT }, + ] + }); + })); + + beforeEach(() => { + service = new LinkHeadService(TestBed.inject(RendererFactory2), TestBed.inject(DOCUMENT)); + }); + + describe('link', () => { + it('should create a link tag', () => { + const link = service.addTag({ + href: 'test', + type: 'application/atom+xml', + rel: 'alternate', + title: 'Sitewide Atom feed' + }); + expect(link).not.toBeUndefined(); + }); + }); + +}); diff --git a/src/app/core/services/link-head.service.ts b/src/app/core/services/link-head.service.ts index af5dddec8d..39552a44d3 100644 --- a/src/app/core/services/link-head.service.ts +++ b/src/app/core/services/link-head.service.ts @@ -12,7 +12,13 @@ export class LinkHeadService { ) { } - addTag(tag: LinkDefinition, forceCreation?: boolean) { + + /** + * Method to create a Link tag in the HEAD of the html. + * @param tag LinkDefition is the paramaters to define a link tag. + * @returns Link tag that was created + */ + addTag(tag: LinkDefinition) { try { const renderer = this.rendererFactory.createRenderer(this.document, { @@ -23,7 +29,8 @@ export class LinkHeadService { }); const link = renderer.createElement('link'); - + console.log(tag); + console.log(link); const head = this.document.head; if (head === null) { @@ -35,12 +42,16 @@ export class LinkHeadService { }); renderer.appendChild(head, link); - + return renderer; } catch (e) { console.error('Error within linkService : ', e); } } + /** + * Removes a link tag in header based on the given attrSelector. + * @param attrSelector The attr assigned to a link tag which will be used to determine what link to remove. + */ removeTag(attrSelector: string) { if (attrSelector) { try { From 2b9383d2d59c47d6f90a2f5a43fdabd43d351d14 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Mon, 18 Apr 2022 12:33:29 -0400 Subject: [PATCH 328/435] remove unused import from rebase --- src/app/shared/shared.module.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c6cc3d2bd9..c70aff6192 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -174,10 +174,6 @@ import { DsSelectComponent } from './ds-select/ds-select.component'; import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'; import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; import { RSSComponent } from './rss-feed/rss.component'; -import { SearchObjects } from './search/models/search-objects.model'; -import { FacetConfigResponse } from './search/models/facet-config-response.model'; -import { SearchResult } from './search/models/search-result.model'; -import { FacetValues } from './search/models/facet-values.model'; const MODULES = [ CommonModule, From 0bf0e1f2742aa253d4a442a83eebb64ea680902a Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Mon, 18 Apr 2022 13:30:35 -0400 Subject: [PATCH 329/435] add providers to browseby --- .../browse-by/browse-by.component.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/app/shared/browse-by/browse-by.component.spec.ts b/src/app/shared/browse-by/browse-by.component.spec.ts index d71d100ca5..8bda44b11c 100644 --- a/src/app/shared/browse-by/browse-by.component.spec.ts +++ b/src/app/shared/browse-by/browse-by.component.spec.ts @@ -38,6 +38,13 @@ import { HostWindowService } from '../host-window.service'; import { RouteService } from '../../core/services/route.service'; import { routeServiceStub } from '../testing/route-service.stub'; import SpyObj = jasmine.SpyObj; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { createPaginatedList } from '../testing/utils.test'; +import { LinkHeadService } from '../../core/services/link-head.service'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; +import { SearchConfigurationServiceStub } from '../testing/search-configuration-service.stub'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; @listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'custom') @Component({ @@ -73,6 +80,25 @@ describe('BrowseByComponent', () => { ]; const mockItemsRD$ = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockItems)); + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '' + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); + const paginationConfig = Object.assign(new PaginationComponentOptions(), { id: 'test-pagination', currentPage: 1, @@ -103,6 +129,10 @@ describe('BrowseByComponent', () => { ], declarations: [], providers: [ + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: GroupDataService, useValue: groupDataService }, { provide: PaginationService, useValue: paginationService }, { provide: MockThemedBrowseEntryListElementComponent }, { provide: ThemeService, useValue: themeService }, From be8a8f5f6bf9359eecc8034b22c522fe9ac1389f Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Wed, 27 Apr 2022 16:13:01 -0400 Subject: [PATCH 330/435] Add typedocs and websvc.opensearch.svccontext --- src/app/core/services/link-head.service.ts | 2 - src/app/shared/rss-feed/rss.component.spec.ts | 6 +-- src/app/shared/rss-feed/rss.component.ts | 53 +++++++++++++------ src/app/shared/shared.module.ts | 1 + src/environments/environment.dev.ts | 17 ------ src/environments/environment.prod.ts | 17 ------ 6 files changed, 41 insertions(+), 55 deletions(-) delete mode 100644 src/environments/environment.dev.ts delete mode 100644 src/environments/environment.prod.ts diff --git a/src/app/core/services/link-head.service.ts b/src/app/core/services/link-head.service.ts index 39552a44d3..d608618ca4 100644 --- a/src/app/core/services/link-head.service.ts +++ b/src/app/core/services/link-head.service.ts @@ -29,8 +29,6 @@ export class LinkHeadService { }); const link = renderer.createElement('link'); - console.log(tag); - console.log(link); const head = this.document.head; if (head === null) { diff --git a/src/app/shared/rss-feed/rss.component.spec.ts b/src/app/shared/rss-feed/rss.component.spec.ts index b304abda83..fc19c65e60 100644 --- a/src/app/shared/rss-feed/rss.component.spec.ts +++ b/src/app/shared/rss-feed/rss.component.spec.ts @@ -96,17 +96,17 @@ describe('RssComponent', () => { }); it('should formulate the correct url given params in url', () => { - const route = comp.formulateRoute(uuid, options, query); + const route = comp.formulateRoute(uuid, 'opensearch', options, query); expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&sort=dc.title&sort_direction=DESC&query=test'); }); it('should skip uuid if its null', () => { - const route = comp.formulateRoute(null, options, query); + const route = comp.formulateRoute(null, 'opensearch', options, query); expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=test'); }); it('should default to query * if none provided', () => { - const route = comp.formulateRoute(null, options, null); + const route = comp.formulateRoute(null, 'opensearch', options, null); expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=*'); }); }); diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 11286e19e3..456e627941 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -45,10 +45,17 @@ export class RSSComponent implements OnInit, OnDestroy { private router: Router, protected paginationService: PaginationService) { } + /** + * Removes the linktag created when the component gets removed from the page. + */ ngOnDestroy(): void { this.linkHeadService.removeTag("rel='alternate'"); } + + /** + * Generates the link tags and the url to opensearch when the component is loaded. + */ ngOnInit(): void { this.configuration$ = this.searchConfigurationService.getCurrentConfiguration('default'); @@ -58,26 +65,36 @@ export class RSSComponent implements OnInit, OnDestroy { const enabled = (result.payload.values[0] === 'true'); this.isEnabled$.next(enabled); }); - - this.searchConfigurationService.getCurrentQuery('').subscribe((query) => { - this.sortOption$ = this.paginationService.getCurrentSort(this.searchConfigurationService.paginationID, null, true); - this.sortOption$.subscribe((sort) => { - this.uuid = this.groupDataService.getUUIDFromString(this.router.url); - - const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, sort, query); - this.addLinks(route); - this.linkHeadService.addTag({ - href: environment.rest.baseUrl + '/opensearch/service', - type: 'application/atom+xml', - rel: 'search', - title: 'Dspace' + this.configurationService.findByPropertyName('websvc.opensearch.svccontext').pipe( + getFirstCompletedRemoteData(), + ).subscribe((url) => { + this.searchConfigurationService.getCurrentQuery('').subscribe((query) => { + this.sortOption$ = this.paginationService.getCurrentSort(this.searchConfigurationService.paginationID, null, true); + this.sortOption$.subscribe((sort) => { + this.uuid = this.groupDataService.getUUIDFromString(this.router.url); + const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, url.payload.values[0], sort, query); + this.addLinks(route); + this.linkHeadService.addTag({ + href: environment.rest.baseUrl + '/' + url.payload.values[0] + '/service', + type: 'application/atom+xml', + rel: 'search', + title: 'Dspace' + }); + this.route$ = new BehaviorSubject(route); }); - this.route$ = new BehaviorSubject(route); }); }); } - formulateRoute(uuid: string, sort: SortOptions, query: string): string { + /** + * Function created a route given the different params available to opensearch + * @param uuid The uuid if a scope is present + * @param opensearch openSearch uri + * @param sort The sort options for the opensearch request + * @param query The query string that was provided in the search + * @returns The combine URL to opensearch + */ + formulateRoute(uuid: string, opensearch: string, sort: SortOptions, query: string): string { let route = 'search?format=atom'; if (uuid) { route += `&scope=${uuid}`; @@ -90,10 +107,14 @@ export class RSSComponent implements OnInit, OnDestroy { } else { route += `&query=*`; } - route = '/opensearch/' + route; + route = '/' + opensearch +'/' + route; return route; } + /** + * Creates tags in the header of the page + * @param route The composed url to opensearch + */ addLinks(route: string): void { this.linkHeadService.addTag({ href: route, diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c70aff6192..7c6fe6657a 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -174,6 +174,7 @@ import { DsSelectComponent } from './ds-select/ds-select.component'; import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'; import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; import { RSSComponent } from './rss-feed/rss.component'; +import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component'; const MODULES = [ CommonModule, diff --git a/src/environments/environment.dev.ts b/src/environments/environment.dev.ts deleted file mode 100644 index 999abd32ee..0000000000 --- a/src/environments/environment.dev.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const environment = { - ui: { - ssl: false, - host: 'localhost', - port: 18080, - nameSpace: '/' - }, - rest: { - ssl: false, - host: 'localhost', - port: 8080, - nameSpace: '/server' - }, - universal: { - preboot: false - } -}; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts deleted file mode 100644 index c31da7b791..0000000000 --- a/src/environments/environment.prod.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const environment = { - ui: { - ssl: false, - host: 'localhost', - port: 18080, - nameSpace: '/' - }, - rest: { - ssl: false, - host: 'localhost', - port: 8080, - nameSpace: '/server' - }, - universal: { - preboot: true - } -}; From 90f1fc186c12719fe5f62f2606ef6daf29d22777 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Thu, 28 Apr 2022 09:36:09 -0400 Subject: [PATCH 331/435] Link fixes --- src/app/shared/rss-feed/rss.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 456e627941..d397019995 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -89,7 +89,7 @@ export class RSSComponent implements OnInit, OnDestroy { /** * Function created a route given the different params available to opensearch * @param uuid The uuid if a scope is present - * @param opensearch openSearch uri + * @param opensearch openSearch uri * @param sort The sort options for the opensearch request * @param query The query string that was provided in the search * @returns The combine URL to opensearch @@ -107,7 +107,7 @@ export class RSSComponent implements OnInit, OnDestroy { } else { route += `&query=*`; } - route = '/' + opensearch +'/' + route; + route = '/' + opensearch + '/' + route; return route; } From 8785270363a4d54ec1bc22d8c90571b7f9ba081d Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Thu, 28 Apr 2022 15:51:21 -0400 Subject: [PATCH 332/435] w2p-86403 Changed to subscribe and unsubscribe on destory --- src/app/shared/rss-feed/rss.component.ts | 53 ++++++++++++++---------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index d397019995..56ed052a9f 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -5,7 +5,7 @@ import { OnInit, ViewEncapsulation } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { GroupDataService } from '../../core/eperson/group-data.service'; import { LinkHeadService } from '../../core/services/link-head.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; @@ -15,6 +15,9 @@ import { SearchConfigurationService } from '../../core/shared/search/search-conf import { SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginationService } from '../../core/pagination/pagination.service'; import { Router } from '@angular/router'; +import { map, switchMap } from 'rxjs/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; /** @@ -32,12 +35,14 @@ export class RSSComponent implements OnInit, OnDestroy { route$: BehaviorSubject; - isEnabled$: BehaviorSubject = new BehaviorSubject(false); + isEnabled$: BehaviorSubject = new BehaviorSubject(null); uuid: string; configuration$: Observable; sortOption$: Observable; + subs: Subscription[] = []; + constructor(private groupDataService: GroupDataService, private linkHeadService: LinkHeadService, private configurationService: ConfigurationDataService, @@ -50,6 +55,9 @@ export class RSSComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.linkHeadService.removeTag("rel='alternate'"); + this.subs.forEach(sub => { + sub.unsubscribe(); + }); } @@ -59,31 +67,32 @@ export class RSSComponent implements OnInit, OnDestroy { ngOnInit(): void { this.configuration$ = this.searchConfigurationService.getCurrentConfiguration('default'); - this.configurationService.findByPropertyName('websvc.opensearch.enable').pipe( + this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.enable').pipe( getFirstCompletedRemoteData(), ).subscribe((result) => { const enabled = (result.payload.values[0] === 'true'); this.isEnabled$.next(enabled); - }); - this.configurationService.findByPropertyName('websvc.opensearch.svccontext').pipe( + })); + this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.svccontext').pipe( getFirstCompletedRemoteData(), - ).subscribe((url) => { - this.searchConfigurationService.getCurrentQuery('').subscribe((query) => { - this.sortOption$ = this.paginationService.getCurrentSort(this.searchConfigurationService.paginationID, null, true); - this.sortOption$.subscribe((sort) => { - this.uuid = this.groupDataService.getUUIDFromString(this.router.url); - const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, url.payload.values[0], sort, query); - this.addLinks(route); - this.linkHeadService.addTag({ - href: environment.rest.baseUrl + '/' + url.payload.values[0] + '/service', - type: 'application/atom+xml', - rel: 'search', - title: 'Dspace' - }); - this.route$ = new BehaviorSubject(route); - }); + map((response: RemoteData) => response.payload.values[0]), + switchMap((openSearchUri: string) => + this.searchConfigurationService.paginatedSearchOptions.pipe( + map((searchOptions: PaginatedSearchOptions) => ({ openSearchUri, searchOptions })) + ) + ), + ).subscribe(({ openSearchUri, searchOptions }) => { + this.uuid = this.groupDataService.getUUIDFromString(this.router.url); + const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, openSearchUri, searchOptions.sort, searchOptions.query); + this.addLinks(route); + this.linkHeadService.addTag({ + href: environment.rest.baseUrl + '/' + openSearchUri + '/service', + type: 'application/atom+xml', + rel: 'search', + title: 'Dspace' }); - }); + this.route$ = new BehaviorSubject(route); + })); } /** @@ -99,7 +108,7 @@ export class RSSComponent implements OnInit, OnDestroy { if (uuid) { route += `&scope=${uuid}`; } - if (sort.direction && sort.field) { + if (sort && sort.direction && sort.field) { route += `&sort=${sort.field}&sort_direction=${sort.direction}`; } if (query) { From 6cd22fa004b52077ad63c02cd302c1c4d4fb1342 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Wed, 4 May 2022 15:27:10 -0400 Subject: [PATCH 333/435] w2p-86403 prevent from displaying on pages where rss feed doesn't make sense --- src/app/shared/rss-feed/rss.component.html | 2 +- src/app/shared/rss-feed/rss.component.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/shared/rss-feed/rss.component.html b/src/app/shared/rss-feed/rss.component.html index 8868539b5c..895a9c2f97 100644 --- a/src/app/shared/rss-feed/rss.component.html +++ b/src/app/shared/rss-feed/rss.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 56ed052a9f..20fb040013 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -120,6 +120,17 @@ export class RSSComponent implements OnInit, OnDestroy { return route; } + /** + * Check if the router url contains the specified route + * + * @param {string} route + * @returns + * @memberof MyComponent + */ + hasRoute(route: string) { + return this.router.url.includes(route); + } + /** * Creates tags in the header of the page * @param route The composed url to opensearch From eacbcfd15d803ea02dede2530934458f336a0e46 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Fri, 6 May 2022 10:15:51 -0400 Subject: [PATCH 334/435] Change to withNotEmptyPayload to pass through if its empty --- src/app/shared/rss-feed/rss.component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 20fb040013..880ed8b558 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -9,7 +9,7 @@ import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { GroupDataService } from '../../core/eperson/group-data.service'; import { LinkHeadService } from '../../core/services/link-head.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; -import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataWithNotEmptyPayload } from '../../core/shared/operators'; import { environment } from '../../../../src/environments/environment'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SortOptions } from '../../core/cache/models/sort-options.model'; @@ -68,14 +68,14 @@ export class RSSComponent implements OnInit, OnDestroy { this.configuration$ = this.searchConfigurationService.getCurrentConfiguration('default'); this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.enable').pipe( - getFirstCompletedRemoteData(), + getFirstSucceededRemoteDataWithNotEmptyPayload(), ).subscribe((result) => { - const enabled = (result.payload.values[0] === 'true'); + const enabled = (result.values[0] === 'true'); this.isEnabled$.next(enabled); })); this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.svccontext').pipe( - getFirstCompletedRemoteData(), - map((response: RemoteData) => response.payload.values[0]), + getFirstSucceededRemoteDataWithNotEmptyPayload(), + map((result) => result.values[0]), switchMap((openSearchUri: string) => this.searchConfigurationService.paginatedSearchOptions.pipe( map((searchOptions: PaginatedSearchOptions) => ({ openSearchUri, searchOptions })) From cb1b7ceb0a8e134fc1b277de97b83c34931b2115 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Fri, 6 May 2022 10:17:30 -0400 Subject: [PATCH 335/435] remove rss feed from search --- src/app/shared/rss-feed/rss.component.html | 2 +- src/app/shared/rss-feed/rss.component.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/shared/rss-feed/rss.component.html b/src/app/shared/rss-feed/rss.component.html index 895a9c2f97..e86e72d15a 100644 --- a/src/app/shared/rss-feed/rss.component.html +++ b/src/app/shared/rss-feed/rss.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 880ed8b558..1d76daabf1 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -9,14 +9,13 @@ import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { GroupDataService } from '../../core/eperson/group-data.service'; import { LinkHeadService } from '../../core/services/link-head.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataWithNotEmptyPayload } from '../../core/shared/operators'; +import { getFirstSucceededRemoteDataWithNotEmptyPayload } from '../../core/shared/operators'; import { environment } from '../../../../src/environments/environment'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginationService } from '../../core/pagination/pagination.service'; import { Router } from '@angular/router'; import { map, switchMap } from 'rxjs/operators'; -import { RemoteData } from '../../core/data/remote-data'; import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; From 79bf27c659aff1fb798954ce826dcb949dcdbf69 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Fri, 6 May 2022 14:03:14 -0400 Subject: [PATCH 336/435] Revert to getFirstCompletedRemoteData but check if payload has completed --- src/app/shared/rss-feed/rss.component.ts | 25 +++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 1d76daabf1..9a101e8310 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -9,7 +9,7 @@ import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { GroupDataService } from '../../core/eperson/group-data.service'; import { LinkHeadService } from '../../core/services/link-head.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; -import { getFirstSucceededRemoteDataWithNotEmptyPayload } from '../../core/shared/operators'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { environment } from '../../../../src/environments/environment'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SortOptions } from '../../core/cache/models/sort-options.model'; @@ -17,6 +17,7 @@ import { PaginationService } from '../../core/pagination/pagination.service'; import { Router } from '@angular/router'; import { map, switchMap } from 'rxjs/operators'; import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; +import { RemoteData } from '../../core/data/remote-data'; /** @@ -67,20 +68,30 @@ export class RSSComponent implements OnInit, OnDestroy { this.configuration$ = this.searchConfigurationService.getCurrentConfiguration('default'); this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.enable').pipe( - getFirstSucceededRemoteDataWithNotEmptyPayload(), + getFirstCompletedRemoteData(), ).subscribe((result) => { - const enabled = (result.values[0] === 'true'); - this.isEnabled$.next(enabled); + if (result.hasSucceeded) { + const enabled = (result.payload.values[0] === 'true'); + this.isEnabled$.next(enabled); + } })); this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.svccontext').pipe( - getFirstSucceededRemoteDataWithNotEmptyPayload(), - map((result) => result.values[0]), - switchMap((openSearchUri: string) => + getFirstCompletedRemoteData(), + map((result: RemoteData) => { + if (result.hasSucceeded) { + return result.payload.values[0]; + } + return null; + }), + switchMap((openSearchUri: string) => this.searchConfigurationService.paginatedSearchOptions.pipe( map((searchOptions: PaginatedSearchOptions) => ({ openSearchUri, searchOptions })) ) ), ).subscribe(({ openSearchUri, searchOptions }) => { + if (!openSearchUri) { + return null; + } this.uuid = this.groupDataService.getUUIDFromString(this.router.url); const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, openSearchUri, searchOptions.sort, searchOptions.query); this.addLinks(route); From 0791287cf9265ff9d329085f5dd9172335c75ca6 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Fri, 6 May 2022 14:09:08 -0400 Subject: [PATCH 337/435] lint fixes --- src/app/core/core.module.ts | 3 --- src/app/shared/rss-feed/rss.component.ts | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index f6cde90253..27e2326818 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -164,12 +164,9 @@ import { SequenceService } from './shared/sequence.service'; import { CoreState } from './core-state.model'; import { GroupDataService } from './eperson/group-data.service'; import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; -<<<<<<< HEAD import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model'; import { AccessStatusDataService } from './data/access-status-data.service'; -======= import { LinkHeadService } from './services/link-head.service'; ->>>>>>> 354768d98 (w2p-85140 ds-rss component now adds a button to all search pages and community and collection, link-head service adds the rss to the head element) /** * When not in production, endpoint responses can be mocked for testing purposes diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 9a101e8310..b694280e3c 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -77,13 +77,13 @@ export class RSSComponent implements OnInit, OnDestroy { })); this.subs.push(this.configurationService.findByPropertyName('websvc.opensearch.svccontext').pipe( getFirstCompletedRemoteData(), - map((result: RemoteData) => { + map((result: RemoteData) => { if (result.hasSucceeded) { return result.payload.values[0]; } return null; }), - switchMap((openSearchUri: string) => + switchMap((openSearchUri: string) => this.searchConfigurationService.paginatedSearchOptions.pipe( map((searchOptions: PaginatedSearchOptions) => ({ openSearchUri, searchOptions })) ) From 42c459a51d5ebcf2eff3423fe239968e24dfcd9c Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Thu, 12 May 2022 16:13:43 +0200 Subject: [PATCH 338/435] [CST-5303] added labels into i18n --- src/assets/i18n/en.json5 | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 9bdf41346d..bb2bcf8ebc 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2158,6 +2158,22 @@ "item.preview.dc.title": "Title:", + "item.preview.dc.type": "Type:", + + "item.preview.oaire.citation.issue" : "Issue", + + "item.preview.oaire.citation.volume" : "Volume", + + "item.preview.dc.relation.issn" : "ISSN", + + "item.preview.dc.identifier.isbn" : "ISBN", + + "item.preview.dc.identifier": "Identifier:", + + "item.preview.dc.relation.ispartof" : "Journal or Serie", + + "item.preview.dc.identifier.doi" : "DOI", + "item.preview.person.familyName": "Surname:", "item.preview.person.givenName": "Name:", @@ -3568,6 +3584,22 @@ "submission.import-external.source.arxiv": "arXiv", + "submission.import-external.source.ads": "NASA/ADS", + + "submission.import-external.source.cinii": "CiNii", + + "submission.import-external.source.crossref": "CrossRef", + + "submission.import-external.source.scielo": "SciELO", + + "submission.import-external.source.scopus": "Scopus", + + "submission.import-external.source.vufind": "VuFind", + + "submission.import-external.source.wos": "Web Of Science", + + "submission.import-external.source.epo": "European Patent Office (EPO)", + "submission.import-external.source.loading": "Loading ...", "submission.import-external.source.sherpaJournal": "SHERPA Journals", @@ -3582,10 +3614,14 @@ "submission.import-external.source.pubmed": "Pubmed", + "submission.import-external.source.pubmedeu": "Pubmed Europe", + "submission.import-external.source.lcname": "Library of Congress Names", "submission.import-external.preview.title": "Item Preview", + "submission.import-external.preview.title.Publication": "Publication Preview", + "submission.import-external.preview.subtitle": "The metadata below was imported from an external source. It will be pre-filled when you start the submission.", "submission.import-external.preview.button.import": "Start submission", @@ -3824,6 +3860,18 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.arxiv": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.crossref": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.epo": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.scopus": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.scielo": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.wos": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title": "Search Results", + "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.confirm": "Save a new name variant", From c1f64ff1ef9a6846b9926968ccfd0d8ec2d548f5 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 12 May 2022 18:50:25 +0200 Subject: [PATCH 339/435] [CST-5329] Add help text to validate only checkbox --- .../metadata-import-page.component.html | 15 +++++++++++---- src/assets/i18n/en.json5 | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html index 2c710935d5..24901cc11d 100644 --- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html +++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html @@ -1,10 +1,17 @@

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

-

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

+
+
+ + +
+ + {{'admin.metadata-import.page.validateOnly.hint' | translate}} + +
Date: Fri, 13 May 2022 13:32:33 -0400 Subject: [PATCH 340/435] for rss sort id should be date.accessioned so defaulted to that when its the case --- src/app/shared/rss-feed/rss.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index b694280e3c..3fdb859bdc 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -118,7 +118,7 @@ export class RSSComponent implements OnInit, OnDestroy { if (uuid) { route += `&scope=${uuid}`; } - if (sort && sort.direction && sort.field) { + if (sort && sort.direction && sort.field && sort.field !== 'id') { route += `&sort=${sort.field}&sort_direction=${sort.direction}`; } if (query) { From 913d128e165e5ef3f3784a2e25dd0bccab26503d Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 13 May 2022 16:57:28 -0500 Subject: [PATCH 341/435] Fix accessibility issue: link has no discernible text --- src/app/shared/rss-feed/rss.component.html | 2 +- src/assets/i18n/en.json5 | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/shared/rss-feed/rss.component.html b/src/app/shared/rss-feed/rss.component.html index e86e72d15a..91140c50c5 100644 --- a/src/app/shared/rss-feed/rss.component.html +++ b/src/app/shared/rss-feed/rss.component.html @@ -1,5 +1,5 @@
- +
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 110b66c176..520d38cf69 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1401,6 +1401,9 @@ "error.validation.groupExists": "This group already exists", + "feed.description": "Syndication feed", + + "file-section.error.header": "Error obtaining files for this item", From 4c5c99d05d3a5ba0b5abd2add953f14082ebea55 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 16 May 2022 16:44:35 +0200 Subject: [PATCH 342/435] [CST-5535] Add missing i18n label --- src/assets/i18n/en.json5 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 44d2d07305..c129187023 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2503,13 +2503,15 @@ "menu.section.icon.find": "Find menu section", + "menu.section.icon.health": "Health check menu section", + "menu.section.icon.import": "Import menu section", "menu.section.icon.new": "New menu section", "menu.section.icon.pin": "Pin sidebar", - "menu.section.icon.processes": "Processes menu section", + "menu.section.icon.processes": "Processes Health", "menu.section.icon.registries": "Registries menu section", From 0de3c2ed48d6f5861ae008eba84757888e7860fe Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 16 May 2022 16:45:00 +0200 Subject: [PATCH 343/435] [CST-5535] Replace icons --- .../health-info-component.component.html | 6 +++--- .../health-component/health-component.component.html | 4 ++-- .../health-page/health-panel/health-panel.component.html | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.html b/src/app/health-page/health-info/health-info-component/health-info-component.component.html index 55c1b3372f..8ce7595980 100644 --- a/src/app/health-page/health-info/health-info-component/health-info-component.component.html +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.html @@ -1,13 +1,13 @@
-
+
- - + +
diff --git a/src/app/health-page/health-panel/health-component/health-component.component.html b/src/app/health-page/health-panel/health-component/health-component.component.html index 8171917767..4569d06dad 100644 --- a/src/app/health-page/health-panel/health-component/health-component.component.html +++ b/src/app/health-page/health-panel/health-component/health-component.component.html @@ -6,8 +6,8 @@ {{ entry.key | titlecase }}
- - + +
diff --git a/src/app/health-page/health-panel/health-panel.component.html b/src/app/health-page/health-panel/health-panel.component.html index d582fb77f3..eebcfe55ec 100644 --- a/src/app/health-page/health-panel/health-panel.component.html +++ b/src/app/health-page/health-panel/health-panel.component.html @@ -1,14 +1,14 @@

{{'health-page.status' | translate}} :

-
+
- - + +
From 80ff8a517ce95c16ab21b376d2e4b664dbe4b5d4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 16 May 2022 16:46:33 +0200 Subject: [PATCH 344/435] [CST-5535] Rename health-data.service --- src/app/health-page/health-page.component.spec.ts | 4 ++-- src/app/health-page/health-page.component.ts | 4 ++-- .../health-page/{health-data.service.ts => health.service.ts} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/app/health-page/{health-data.service.ts => health.service.ts} (97%) diff --git a/src/app/health-page/health-page.component.spec.ts b/src/app/health-page/health-page.component.spec.ts index 205af8036a..f3847ab092 100644 --- a/src/app/health-page/health-page.component.spec.ts +++ b/src/app/health-page/health-page.component.spec.ts @@ -7,7 +7,7 @@ import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { HealthPageComponent } from './health-page.component'; -import { HealthDataService } from './health-data.service'; +import { HealthService } from './health.service'; import { HealthInfoResponseObj, HealthResponseObj } from '../shared/mocks/health-endpoint.mocks'; import { RawRestResponse } from '../core/dspace-rest/raw-rest-response.model'; import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; @@ -47,7 +47,7 @@ describe('HealthPageComponent', () => { ], declarations: [ HealthPageComponent ], providers: [ - { provide: HealthDataService, useValue: healthService } + { provide: HealthService, useValue: healthService } ] }) .compileComponents(); diff --git a/src/app/health-page/health-page.component.ts b/src/app/health-page/health-page.component.ts index e4f4be7a03..eb07b63add 100644 --- a/src/app/health-page/health-page.component.ts +++ b/src/app/health-page/health-page.component.ts @@ -3,7 +3,7 @@ import { Component, OnInit } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { take } from 'rxjs/operators'; -import { HealthDataService } from './health-data.service'; +import { HealthService } from './health.service'; import { HealthInfoResponse, HealthResponse } from './models/health-component.model'; @Component({ @@ -23,7 +23,7 @@ export class HealthPageComponent implements OnInit { */ healthResponse: BehaviorSubject = new BehaviorSubject(null); - constructor(private healthDataService: HealthDataService) { + constructor(private healthDataService: HealthService) { } /** diff --git a/src/app/health-page/health-data.service.ts b/src/app/health-page/health.service.ts similarity index 97% rename from src/app/health-page/health-data.service.ts rename to src/app/health-page/health.service.ts index bd905006aa..7c238769a1 100644 --- a/src/app/health-page/health-data.service.ts +++ b/src/app/health-page/health.service.ts @@ -8,7 +8,7 @@ import { HALEndpointService } from '../core/shared/hal-endpoint.service'; @Injectable({ providedIn: 'root' }) -export class HealthDataService { +export class HealthService { constructor(protected halService: HALEndpointService, protected restService: DspaceRestService) { } From 4f7e37d348dfeccca1cc7e1f8b7efeb526b11207 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 16 May 2022 18:17:35 +0200 Subject: [PATCH 345/435] [CST-5535] Use accordion for health status components --- .../health-info-component.component.html | 38 ++++++++++++++--- .../health-info-component.component.spec.ts | 4 +- .../health-info/health-info.component.html | 30 ++++++++++++- .../health-info/health-info.component.spec.ts | 8 +++- .../health-info/health-info.component.ts | 12 +++++- .../health-component.component.html | 3 +- .../health-panel/health-panel.component.html | 42 ++++++++++--------- .../health-panel.component.spec.ts | 9 ++-- .../health-panel/health-panel.component.ts | 11 +++-- src/app/shared/mocks/health-endpoint.mocks.ts | 24 ++++++++++- 10 files changed, 139 insertions(+), 42 deletions(-) diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.html b/src/app/health-page/health-info/health-info-component/health-info-component.component.html index 8ce7595980..b16e88564f 100644 --- a/src/app/health-page/health-info/health-info-component/health-info-component.component.html +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.html @@ -1,4 +1,34 @@ - +
+
+
+ +
+ + +
+
+
+
+
+ +
+
+
+
+ +

{{ entry.key | titlecase }} : {{entry.value}}

+
+
+ + + + diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts b/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts index 2297007cd5..437d53a953 100644 --- a/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts @@ -46,7 +46,9 @@ describe('HealthInfoComponentComponent', () => { }); it('should display property', () => { - const components = fixture.debugElement.queryAll(By.css('[data-test="component"]')); + const properties = fixture.debugElement.queryAll(By.css('[data-test="property"]')); + expect(properties.length).toBe(14); + const components = fixture.debugElement.queryAll(By.css('[data-test="info-component"]')); expect(components.length).toBe(4); }); diff --git a/src/app/health-page/health-info/health-info.component.html b/src/app/health-page/health-info/health-info.component.html index e4d29adf54..be69df23b4 100644 --- a/src/app/health-page/health-info/health-info.component.html +++ b/src/app/health-page/health-info/health-info.component.html @@ -1,7 +1,33 @@ -
+ + + +
+ +
+ +
+ + +
+
+
+
+ + + +
+
+ + + + diff --git a/src/app/health-page/health-info/health-info.component.spec.ts b/src/app/health-page/health-info/health-info.component.spec.ts index 3af1d71db3..a7f319b88b 100644 --- a/src/app/health-page/health-info/health-info.component.spec.ts +++ b/src/app/health-page/health-info/health-info.component.spec.ts @@ -4,6 +4,8 @@ import { HealthInfoComponent } from './health-info.component'; import { HealthInfoResponseObj } from '../../shared/mocks/health-endpoint.mocks'; import { ObjNgFor } from '../../shared/utils/object-ngfor.pipe'; import { By } from '@angular/platform-browser'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; describe('HealthInfoComponent', () => { let component: HealthInfoComponent; @@ -11,10 +13,14 @@ describe('HealthInfoComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + imports: [ + NgbAccordionModule, + ], declarations: [ HealthInfoComponent, ObjNgFor - ] + ], + schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); }); diff --git a/src/app/health-page/health-info/health-info.component.ts b/src/app/health-page/health-info/health-info.component.ts index a5fb0b282b..9fddaeb7e4 100644 --- a/src/app/health-page/health-info/health-info.component.ts +++ b/src/app/health-page/health-info/health-info.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { HealthInfoResponse } from '../models/health-component.model'; @@ -7,8 +7,16 @@ import { HealthInfoResponse } from '../models/health-component.model'; templateUrl: './health-info.component.html', styleUrls: ['./health-info.component.scss'] }) -export class HealthInfoComponent { +export class HealthInfoComponent implements OnInit { @Input() healthInfoResponse: HealthInfoResponse; + /** + * The first active panel id + */ + activeId: string; + + ngOnInit(): void { + this.activeId = Object.keys(this.healthInfoResponse)[0]; + } } diff --git a/src/app/health-page/health-panel/health-component/health-component.component.html b/src/app/health-page/health-panel/health-component/health-component.component.html index 4569d06dad..7089fe25c6 100644 --- a/src/app/health-page/health-panel/health-component/health-component.component.html +++ b/src/app/health-page/health-panel/health-component/health-component.component.html @@ -22,6 +22,7 @@
- {{ item.key | titlecase }} : {{item.value}} +

{{ item.key | titlecase }} : {{item.value}}

+
diff --git a/src/app/health-page/health-panel/health-panel.component.html b/src/app/health-page/health-panel/health-panel.component.html index eebcfe55ec..d47a73d820 100644 --- a/src/app/health-page/health-panel/health-panel.component.html +++ b/src/app/health-page/health-panel/health-panel.component.html @@ -1,21 +1,25 @@

{{'health-page.status' | translate}} :

-
-
- -
- - - -
-
-
-
-
- + + + +
+ +
+ +
+ + +
+
-
-
-
+ + + + + + + + diff --git a/src/app/health-page/health-panel/health-panel.component.spec.ts b/src/app/health-page/health-panel/health-panel.component.spec.ts index da392f7ba8..1d9c856ddb 100644 --- a/src/app/health-page/health-panel/health-panel.component.spec.ts +++ b/src/app/health-page/health-panel/health-panel.component.spec.ts @@ -5,14 +5,14 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { NgbCollapseModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { HealthPanelComponent } from './health-panel.component'; import { HealthResponseObj } from '../../shared/mocks/health-endpoint.mocks'; import { ObjNgFor } from '../../shared/utils/object-ngfor.pipe'; -describe('HealthComponent', () => { +describe('HealthPanelComponent', () => { let component: HealthPanelComponent; let fixture: ComponentFixture; @@ -20,7 +20,7 @@ describe('HealthComponent', () => { await TestBed.configureTestingModule({ imports: [ NgbNavModule, - NgbCollapseModule, + NgbAccordionModule, CommonModule, BrowserAnimationsModule, TranslateModule.forRoot({ @@ -42,7 +42,6 @@ describe('HealthComponent', () => { fixture = TestBed.createComponent(HealthPanelComponent); component = fixture.componentInstance; component.healthResponse = HealthResponseObj; - component.isCollapsed = false; fixture.detectChanges(); }); @@ -50,7 +49,7 @@ describe('HealthComponent', () => { expect(component).toBeTruthy(); }); - it('should render a card for each component', () => { + it('should render a panel for each component', () => { const components = fixture.debugElement.queryAll(By.css('[data-test="component"]')); expect(components.length).toBe(5); }); diff --git a/src/app/health-page/health-panel/health-panel.component.ts b/src/app/health-page/health-panel/health-panel.component.ts index 549544c370..8bb670e67f 100644 --- a/src/app/health-page/health-panel/health-panel.component.ts +++ b/src/app/health-page/health-panel/health-panel.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { HealthResponse } from '../models/health-component.model'; @Component({ @@ -6,7 +6,7 @@ import { HealthResponse } from '../models/health-component.model'; templateUrl: './health-panel.component.html', styleUrls: ['./health-panel.component.scss'] }) -export class HealthPanelComponent { +export class HealthPanelComponent implements OnInit { /** * Health endpoint response @@ -14,8 +14,11 @@ export class HealthPanelComponent { @Input() healthResponse: HealthResponse; /** - * A boolean representing if div should start collapsed + * The first active panel id */ - public isCollapsed = true; + activeId: string; + ngOnInit(): void { + this.activeId = Object.keys(this.healthResponse.components)[0]; + } } diff --git a/src/app/shared/mocks/health-endpoint.mocks.ts b/src/app/shared/mocks/health-endpoint.mocks.ts index 9bd3956139..a9246d91a1 100644 --- a/src/app/shared/mocks/health-endpoint.mocks.ts +++ b/src/app/shared/mocks/health-endpoint.mocks.ts @@ -134,7 +134,27 @@ export const HealthInfoComponentOne: HealthInfoComponent = { 'name': 'DSpace at My University', 'dir': '/home/giuseppe/development/java/install/dspace7-review', 'url': 'http://localhost:8080/server', - 'db': 'jdbc:postgresql://localhost:5432/dspace7' + 'db': 'jdbc:postgresql://localhost:5432/dspace7', + 'solr': { + 'server': 'http://localhost:8983/solr', + 'prefix': '' + }, + 'mail': { + 'server': 'smtp.example.com', + 'from-address': 'dspace-noreply@myu.edu', + 'feedback-recipient': 'dspace-help@myu.edu', + 'mail-admin': 'dspace-help@myu.edu', + 'mail-helpdesk': 'dspace-help@myu.edu', + 'alert-recipient': 'dspace-help@myu.edu' + }, + 'cors': { + 'allowed-origins': 'http://localhost:4000' + }, + 'ui': { + 'url': 'http://localhost:4000' + } }; -export const HealthInfoComponentTwo = '7.3-SNAPSHOT'; +export const HealthInfoComponentTwo = { + 'version': '7.3-SNAPSHOT' +}; From c508e0035e46c977bb7b0fad2163eff5109c3d81 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 16 May 2022 18:49:11 +0200 Subject: [PATCH 346/435] [CST-5535] Add tooltip for status icons --- .../health-status/health-status.component.html | 12 +++++++++--- src/assets/i18n/en.json5 | 8 ++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/app/health-page/health-panel/health-status/health-status.component.html b/src/app/health-page/health-panel/health-status/health-status.component.html index fdd726cddf..38a6f72601 100644 --- a/src/app/health-page/health-panel/health-status/health-status.component.html +++ b/src/app/health-page/health-panel/health-status/health-status.component.html @@ -1,6 +1,12 @@ - - - + + + diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index c129187023..78661ced76 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1567,6 +1567,14 @@ + "health-page.status.ok.info": "Operational", + + "health-page.status.error.info": "Problems detected", + + "health-page.status.warning.info": "Possible issues detected", + + + "home.description": "", "home.breadcrumbs": "Home", From 73573793a0eacc52789c4df6b7a653040393f407 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 16 May 2022 18:50:07 +0200 Subject: [PATCH 347/435] [CST-5535] Add i18n keys for section's headers --- .../health-info/health-info.component.html | 2 +- .../health-panel/health-panel.component.html | 2 +- src/assets/i18n/en.json5 | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/health-page/health-info/health-info.component.html b/src/app/health-page/health-info/health-info.component.html index be69df23b4..12764ead45 100644 --- a/src/app/health-page/health-info/health-info.component.html +++ b/src/app/health-page/health-info/health-info.component.html @@ -5,7 +5,7 @@
diff --git a/src/app/health-page/health-panel/health-panel.component.html b/src/app/health-page/health-panel/health-panel.component.html index d47a73d820..d095ce2f6d 100644 --- a/src/app/health-page/health-panel/health-panel.component.html +++ b/src/app/health-page/health-panel/health-panel.component.html @@ -5,7 +5,7 @@
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 78661ced76..c22ec1c3ee 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1566,6 +1566,21 @@ "grant-request-copy.success": "Successfully granted item request", + "health-page.section.db.title": "Database", + + "health-page.section.geoIp.title": "GeoIp", + + "health-page.section.solrAuthorityCore.title": "Sor: authority core", + + "health-page.section.solrOaiCore.title": "Sor: oai core", + + "health-page.section.solrSearchCore.title": "Sor: search core", + + "health-page.section.solrStatisticsCore.title": "Sor: statistics core", + + "health-page.section-info.app.title": "Application Backend", + + "health-page.section-info.java.title": "Java", "health-page.status.ok.info": "Operational", From 74b68a5e154f1285ad3e7ec9d13fcb30590378d4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 16 May 2022 19:19:00 +0200 Subject: [PATCH 348/435] [CST-5535] Add fallback message on rest request error --- .../health-page/health-page.component.html | 45 ++++++++++--------- src/app/health-page/health-page.component.ts | 11 +++-- src/assets/i18n/en.json5 | 2 + 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/app/health-page/health-page.component.html b/src/app/health-page/health-page.component.html index 6ec9abddcb..0647620c73 100644 --- a/src/app/health-page/health-page.component.html +++ b/src/app/health-page/health-page.component.html @@ -1,21 +1,26 @@ -
- -
+
+ +
+ + diff --git a/src/app/health-page/health-page.component.ts b/src/app/health-page/health-page.component.ts index eb07b63add..a92e72744b 100644 --- a/src/app/health-page/health-page.component.ts +++ b/src/app/health-page/health-page.component.ts @@ -30,12 +30,15 @@ export class HealthPageComponent implements OnInit { * Retrieve responses from rest */ ngOnInit(): void { - this.healthDataService.getHealth().pipe(take(1)).subscribe((data: any) => { - this.healthResponse.next(data.payload); + this.healthDataService.getHealth().pipe(take(1)).subscribe({ + next: (data: any) => { this.healthResponse.next(data.payload); }, + error: () => { this.healthResponse.next(null); } }); - this.healthDataService.getInfo().pipe(take(1)).subscribe((data) => { - this.healthInfoResponse.next(data.payload); + this.healthDataService.getInfo().pipe(take(1)).subscribe({ + next: (data: any) => { this.healthInfoResponse.next(data.payload); }, + error: () => { this.healthInfoResponse.next(null); } }); + } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4fcdbff335..216b29fcd0 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1583,6 +1583,8 @@ "grant-request-copy.success": "Successfully granted item request", + "health-page.error.msg": "The health check service is temporarily unavailable", + "health-page.section.db.title": "Database", "health-page.section.geoIp.title": "GeoIp", From 1f9b8ba92a4fcf09e2f0c6cb8661f7b8b6fdd637 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 17 May 2022 11:16:46 +0200 Subject: [PATCH 349/435] Fix issue where you'd get stuck in an infinite redirect loop when going to the page of an item with an entity type containing certain special characters --- src/app/item-page/item-page.resolver.spec.ts | 87 ++++++++++++++++++++ src/app/item-page/item-page.resolver.ts | 10 ++- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/app/item-page/item-page.resolver.spec.ts diff --git a/src/app/item-page/item-page.resolver.spec.ts b/src/app/item-page/item-page.resolver.spec.ts new file mode 100644 index 0000000000..533f97d0c0 --- /dev/null +++ b/src/app/item-page/item-page.resolver.spec.ts @@ -0,0 +1,87 @@ +import { ItemPageResolver } from './item-page.resolver'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { DSpaceObject } from '../core/shared/dspace-object.model'; +import { MetadataValueFilter } from '../core/shared/metadata.models'; +import { first } from 'rxjs/operators'; +import { Router } from '@angular/router'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('ItemPageResolver', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule.withRoutes([{ + path: 'entities/:entity-type/:id', + component: {} as any + }])] + }); + }); + + describe('resolve', () => { + let resolver: ItemPageResolver; + let itemService: any; + let store: any; + let router: any; + + const uuid = '1234-65487-12354-1235'; + let item: DSpaceObject; + + function runTestsWithEntityType(entityType: string) { + beforeEach(() => { + router = TestBed.inject(Router); + item = Object.assign(new DSpaceObject(), { + uuid: uuid, + firstMetadataValue(_keyOrKeys: string | string[], _valueFilter?: MetadataValueFilter): string { + return entityType; + } + }); + itemService = { + findById: (_id: string) => createSuccessfulRemoteDataObject$(item) + }; + store = jasmine.createSpyObj('store', { + dispatch: {}, + }); + resolver = new ItemPageResolver(itemService, store, router); + }); + + it('should redirect to the correct route for the entity type', (done) => { + spyOn(item, 'firstMetadataValue').and.returnValue(entityType); + spyOn(router, 'navigateByUrl').and.callThrough(); + + resolver.resolve({ params: { id: uuid } } as any, { url: router.parseUrl(`/items/${uuid}`).toString() } as any) + .pipe(first()) + .subscribe( + () => { + expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl(`/entities/${entityType}/${uuid}`).toString()); + done(); + } + ); + }); + + it('should not redirect if we’re already on the correct route', (done) => { + spyOn(item, 'firstMetadataValue').and.returnValue(entityType); + spyOn(router, 'navigateByUrl').and.callThrough(); + + resolver.resolve({ params: { id: uuid } } as any, { url: router.parseUrl(`/entities/${entityType}/${uuid}`).toString() } as any) + .pipe(first()) + .subscribe( + () => { + expect(router.navigateByUrl).not.toHaveBeenCalled(); + done(); + } + ); + }); + } + + describe('when normal entity type is provided', () => { + runTestsWithEntityType('publication'); + }); + + describe('when entity type contains a special character', () => { + runTestsWithEntityType('alligator,loki'); + runTestsWithEntityType('🐊'); + runTestsWithEntityType(' '); + }); + + }); +}); diff --git a/src/app/item-page/item-page.resolver.ts b/src/app/item-page/item-page.resolver.ts index 7edffc5357..e9b287406e 100644 --- a/src/app/item-page/item-page.resolver.ts +++ b/src/app/item-page/item-page.resolver.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { ItemDataService } from '../core/data/item-data.service'; @@ -35,8 +35,14 @@ export class ItemPageResolver extends ItemResolver { return super.resolve(route, state).pipe( map((rd: RemoteData) => { if (rd.hasSucceeded && hasValue(rd.payload)) { - const itemRoute = getItemPageRoute(rd.payload); const thisRoute = state.url; + + // Angular uses a custom function for encodeURIComponent, (e.g. it doesn't encode commas + // or semicolons) and thisRoute has been encoded with that function. If we want to compare + // it with itemRoute, we have to run itemRoute through Angular's version as well to ensure + // the same characters are encoded the same way. + const itemRoute = this.router.parseUrl(getItemPageRoute(rd.payload)).toString(); + if (!thisRoute.startsWith(itemRoute)) { const itemId = rd.payload.uuid; const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length); From 881af6449542ee174206d5b10ec27b87f0254219 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Tue, 17 May 2022 16:36:40 +0200 Subject: [PATCH 350/435] [CST-5674] POST replaced with PUT --- .../resource-policy.service.ts | 4 +-- .../form/resource-policy-form.component.html | 34 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/app/core/resource-policy/resource-policy.service.ts b/src/app/core/resource-policy/resource-policy.service.ts index ca3951109a..8411647bea 100644 --- a/src/app/core/resource-policy/resource-policy.service.ts +++ b/src/app/core/resource-policy/resource-policy.service.ts @@ -29,7 +29,7 @@ import { getFirstCompletedRemoteData } from '../shared/operators'; import { CoreState } from '../core-state.model'; import { FindListOptions } from '../data/find-list-options.model'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { PostRequest } from '../data/request.models'; +import { PutRequest } from '../data/request.models'; import { GenericConstructor } from '../shared/generic-constructor'; import { ResponseParsingService } from '../data/parsing.service'; import { StatusCodeOnlyResponseParsingService } from '../data/status-code-only-response-parsing.service'; @@ -260,7 +260,7 @@ export class ResourcePolicyService { return targetEndpoint$.pipe(switchMap((targetEndpoint) => { const resourceEndpoint = resourcePolicyHref + '/' + type; - const request = new PostRequest(requestId, resourceEndpoint, targetEndpoint, options); + const request = new PutRequest(requestId, resourceEndpoint, targetEndpoint, options); Object.assign(request, { getResponseParser(): GenericConstructor { return StatusCodeOnlyResponseParsingService; diff --git a/src/app/shared/resource-policies/form/resource-policy-form.component.html b/src/app/shared/resource-policies/form/resource-policy-form.component.html index f7aad55ce8..66c1fc400e 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.component.html +++ b/src/app/shared/resource-policies/form/resource-policy-form.component.html @@ -8,24 +8,22 @@
- - -
-
+ +

From 618ff0ce19607900bfd165aec9ae7ef01a9b19a2 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Tue, 17 May 2022 17:45:27 +0200 Subject: [PATCH 351/435] [CST-5303] added missing labels --- src/assets/i18n/en.json5 | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index bb2bcf8ebc..5bae41d0a1 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3644,6 +3644,26 @@ "submission.sections.describe.relationship-lookup.external-source.import-button-title.isProjectOfPublication": "Project", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.none = "Import remote item", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Event": "Import remote event", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Product": "Import remote product", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Equipment": "Import remote equipment", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.OrgUnit = "Import remote organizational unit", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Funding": "Import remote fund", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Person = "Import remote person", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Patent": "Import remote patent", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Project = "Import remote project", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Publication = "Import remote publication", + "submission.sections.describe.relationship-lookup.external-source.import-modal.isProjectOfPublication.added.new-entity": "New Entity Added!", "submission.sections.describe.relationship-lookup.external-source.import-modal.isProjectOfPublication.title": "Project", From a732f1534de03fbdc89aad33af769dc50f40fb72 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 18 May 2022 09:33:08 +0200 Subject: [PATCH 352/435] [CST-5303] fix wrong format --- src/assets/i18n/en.json5 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 5bae41d0a1..dffbbd4b7e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3644,7 +3644,7 @@ "submission.sections.describe.relationship-lookup.external-source.import-button-title.isProjectOfPublication": "Project", - "submission.sections.describe.relationship-lookup.external-source.import-button-title.none = "Import remote item", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.none": "Import remote item", "submission.sections.describe.relationship-lookup.external-source.import-button-title.Event": "Import remote event", @@ -3652,17 +3652,17 @@ "submission.sections.describe.relationship-lookup.external-source.import-button-title.Equipment": "Import remote equipment", - "submission.sections.describe.relationship-lookup.external-source.import-button-title.OrgUnit = "Import remote organizational unit", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.OrgUnit": "Import remote organizational unit", "submission.sections.describe.relationship-lookup.external-source.import-button-title.Funding": "Import remote fund", - "submission.sections.describe.relationship-lookup.external-source.import-button-title.Person = "Import remote person", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Person": "Import remote person", "submission.sections.describe.relationship-lookup.external-source.import-button-title.Patent": "Import remote patent", - "submission.sections.describe.relationship-lookup.external-source.import-button-title.Project = "Import remote project", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Project": "Import remote project", - "submission.sections.describe.relationship-lookup.external-source.import-button-title.Publication = "Import remote publication", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Publication": "Import remote publication", "submission.sections.describe.relationship-lookup.external-source.import-modal.isProjectOfPublication.added.new-entity": "New Entity Added!", From d4dc176870d46506ad72670ba391f9f041e1c495 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 18 May 2022 13:04:04 +0200 Subject: [PATCH 353/435] [CST-5535] Add Possibility to have translation also for properties --- .../health-info-component.component.html | 4 ++-- .../health-info-component.component.scss | 3 +++ .../health-info-component.component.spec.ts | 10 +++++++++- .../health-info-component.component.ts | 12 ++++++++++-- .../health-info/health-info.component.html | 4 ++-- .../health-info/health-info.component.scss | 3 +++ .../health-info/health-info.component.spec.ts | 8 ++++++++ .../health-info/health-info.component.ts | 15 +++++++++++++++ .../health-component.component.html | 4 ++-- .../health-component.component.scss | 3 +++ .../health-component.component.spec.ts | 10 +++++++++- .../health-component.component.ts | 17 ++++++++++++++++- .../health-panel/health-panel.component.html | 4 ++-- .../health-panel/health-panel.component.scss | 3 +++ .../health-panel/health-panel.component.ts | 16 ++++++++++++++++ .../health-status.component.spec.ts | 12 ++++++++++++ src/assets/i18n/en.json5 | 2 ++ 17 files changed, 117 insertions(+), 13 deletions(-) diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.html b/src/app/health-page/health-info/health-info-component/health-info-component.component.html index b16e88564f..b607d95f45 100644 --- a/src/app/health-page/health-info/health-info-component/health-info-component.component.html +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.html @@ -1,6 +1,6 @@
-
+
-

{{ entry.key | titlecase }} : {{entry.value}}

+

{{ getPropertyLabel(entry.key) | titlecase }} : {{entry.value}}

diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.scss b/src/app/health-page/health-info/health-info-component/health-info-component.component.scss index e69de29bb2..a6f0e73413 100644 --- a/src/app/health-page/health-info/health-info-component/health-info-component.component.scss +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts b/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts index 437d53a953..b4532415b8 100644 --- a/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.spec.ts @@ -8,6 +8,8 @@ import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; import { HealthInfoComponentComponent } from './health-info-component.component'; import { HealthInfoComponentOne, HealthInfoComponentTwo } from '../../../shared/mocks/health-endpoint.mocks'; import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; describe('HealthInfoComponentComponent', () => { let component: HealthInfoComponentComponent; @@ -18,7 +20,13 @@ describe('HealthInfoComponentComponent', () => { imports: [ CommonModule, NgbCollapseModule, - NoopAnimationsModule + NoopAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) ], declarations: [ HealthInfoComponentComponent, diff --git a/src/app/health-page/health-info/health-info-component/health-info-component.component.ts b/src/app/health-page/health-info/health-info-component/health-info-component.component.ts index b6c31214c8..159462cd6d 100644 --- a/src/app/health-page/health-info/health-info-component/health-info-component.component.ts +++ b/src/app/health-page/health-info/health-info-component/health-info-component.component.ts @@ -1,13 +1,14 @@ import { Component, Input } from '@angular/core'; import { HealthInfoComponent } from '../../models/health-component.model'; +import { HealthComponentComponent } from '../../health-panel/health-component/health-component.component'; @Component({ selector: 'ds-health-info-component', templateUrl: './health-info-component.component.html', styleUrls: ['./health-info-component.component.scss'] }) -export class HealthInfoComponentComponent { +export class HealthInfoComponentComponent extends HealthComponentComponent { /** * The HealthInfoComponent object to display @@ -27,9 +28,16 @@ export class HealthInfoComponentComponent { /** * A boolean representing if div should start collapsed */ - public isCollapsed = true; + public isCollapsed = false; + /** + * Check if the HealthInfoComponent is has only string property or contains object + * + * @param entry The HealthInfoComponent to check + * @return boolean + */ isPlainProperty(entry: HealthInfoComponent | string): boolean { return typeof entry === 'string'; } + } diff --git a/src/app/health-page/health-info/health-info.component.html b/src/app/health-page/health-info/health-info.component.html index 12764ead45..47e4cfb4d2 100644 --- a/src/app/health-page/health-info/health-info.component.html +++ b/src/app/health-page/health-info/health-info.component.html @@ -2,10 +2,10 @@ -
+
diff --git a/src/app/health-page/health-info/health-info.component.scss b/src/app/health-page/health-info/health-info.component.scss index e69de29bb2..a6f0e73413 100644 --- a/src/app/health-page/health-info/health-info.component.scss +++ b/src/app/health-page/health-info/health-info.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-info/health-info.component.spec.ts b/src/app/health-page/health-info/health-info.component.spec.ts index a7f319b88b..5a9b8bf0aa 100644 --- a/src/app/health-page/health-info/health-info.component.spec.ts +++ b/src/app/health-page/health-info/health-info.component.spec.ts @@ -6,6 +6,8 @@ import { ObjNgFor } from '../../shared/utils/object-ngfor.pipe'; import { By } from '@angular/platform-browser'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; describe('HealthInfoComponent', () => { let component: HealthInfoComponent; @@ -15,6 +17,12 @@ describe('HealthInfoComponent', () => { await TestBed.configureTestingModule({ imports: [ NgbAccordionModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) ], declarations: [ HealthInfoComponent, diff --git a/src/app/health-page/health-info/health-info.component.ts b/src/app/health-page/health-info/health-info.component.ts index 9fddaeb7e4..d8c629636b 100644 --- a/src/app/health-page/health-info/health-info.component.ts +++ b/src/app/health-page/health-info/health-info.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { HealthInfoResponse } from '../models/health-component.model'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-health-info', @@ -16,7 +17,21 @@ export class HealthInfoComponent implements OnInit { */ activeId: string; + constructor(private translate: TranslateService) { + } + ngOnInit(): void { this.activeId = Object.keys(this.healthInfoResponse)[0]; } + /** + * Return translated label if exist for the given property + * + * @param property + */ + public getPanelLabel(panelKey: string): string { + const translationKey = `health-page.section-info.${panelKey}.title`; + const translation = this.translate.instant(translationKey); + + return (translation === translationKey) ? panelKey : translation; + } } diff --git a/src/app/health-page/health-panel/health-component/health-component.component.html b/src/app/health-page/health-panel/health-component/health-component.component.html index 7089fe25c6..c254f128d9 100644 --- a/src/app/health-page/health-panel/health-component/health-component.component.html +++ b/src/app/health-page/health-panel/health-component/health-component.component.html @@ -1,6 +1,6 @@
-
+
diff --git a/src/app/health-page/health-panel/health-panel.component.scss b/src/app/health-page/health-panel/health-panel.component.scss index e69de29bb2..a6f0e73413 100644 --- a/src/app/health-page/health-panel/health-panel.component.scss +++ b/src/app/health-page/health-panel/health-panel.component.scss @@ -0,0 +1,3 @@ +.collapse-toggle { + cursor: pointer; +} diff --git a/src/app/health-page/health-panel/health-panel.component.ts b/src/app/health-page/health-panel/health-panel.component.ts index 8bb670e67f..3137334d6f 100644 --- a/src/app/health-page/health-panel/health-panel.component.ts +++ b/src/app/health-page/health-panel/health-panel.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { HealthResponse } from '../models/health-component.model'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-health-panel', @@ -18,7 +19,22 @@ export class HealthPanelComponent implements OnInit { */ activeId: string; + constructor(private translate: TranslateService) { + } + ngOnInit(): void { this.activeId = Object.keys(this.healthResponse.components)[0]; } + + /** + * Return translated label if exist for the given property + * + * @param property + */ + public getPanelLabel(panelKey: string): string { + const translationKey = `health-page.section.${panelKey}.title`; + const translation = this.translate.instant(translationKey); + + return (translation === translationKey) ? panelKey : translation; + } } diff --git a/src/app/health-page/health-panel/health-status/health-status.component.spec.ts b/src/app/health-page/health-panel/health-status/health-status.component.spec.ts index 13df9c23e3..f0f61ebdbb 100644 --- a/src/app/health-page/health-panel/health-status/health-status.component.spec.ts +++ b/src/app/health-page/health-panel/health-status/health-status.component.spec.ts @@ -3,6 +3,9 @@ import { By } from '@angular/platform-browser'; import { HealthStatusComponent } from './health-status.component'; import { HealthStatus } from '../../models/health-component.model'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; describe('HealthStatusComponent', () => { let component: HealthStatusComponent; @@ -10,6 +13,15 @@ describe('HealthStatusComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + imports: [ + NgbTooltipModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], declarations: [ HealthStatusComponent ] }) .compileComponents(); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 216b29fcd0..18a406b77b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1585,6 +1585,8 @@ "health-page.error.msg": "The health check service is temporarily unavailable", + "health-page.property.status": "Status code", + "health-page.section.db.title": "Database", "health-page.section.geoIp.title": "GeoIp", From ef332af17ea62afce19ef83763ac30b356e63289 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 18 May 2022 13:31:09 +0200 Subject: [PATCH 354/435] [CST-5535] Fix issue with error notice message on health page loading --- .../health-page/health-page.component.html | 2 +- src/app/health-page/health-page.component.ts | 30 ++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/app/health-page/health-page.component.html b/src/app/health-page/health-page.component.html index 0647620c73..605927dc55 100644 --- a/src/app/health-page/health-page.component.html +++ b/src/app/health-page/health-page.component.html @@ -1,4 +1,4 @@ -
+