Merge branch 'main' into w2p-94474_clarify-process-state

# Conflicts:
#	src/assets/i18n/pt-BR.json5
#	src/assets/i18n/sv.json5
This commit is contained in:
Jens Vannerum
2022-09-28 18:05:02 +02:00
212 changed files with 8299 additions and 5077 deletions

View File

@@ -174,6 +174,27 @@ browseBy:
fiveYearLimit: 30 fiveYearLimit: 30
# The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) # The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
defaultLowerLimit: 1900 defaultLowerLimit: 1900
# If true, thumbnail images for items will be added to BOTH search and browse result lists.
showThumbnails: true
# The number of entries in a paginated browse results list.
# Rounded to the nearest size in the list of selectable sizes on the
# settings menu.
pageSize: 20
communityList:
# No. of communities to list per expansion (show more)
pageSize: 20
homePage:
recentSubmissions:
# The number of item showing in recent submission components
pageSize: 5
# Sort record of recent submission
sortField: 'dc.date.accessioned'
topLevelCommunityList:
# No. of communities to list per page on the home page
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
pageSize: 5
# Item Config # Item Config
item: item:
@@ -249,7 +270,7 @@ themes:
# The default bundles that should always be displayed as suggestions when you upload a new bundle # The default bundles that should always be displayed as suggestions when you upload a new bundle
bundle: bundle:
- standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ] standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
# Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video'). # Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video').
# For images, this enables a gallery viewer where you can zoom or page through images. # For images, this enables a gallery viewer where you can zoom or page through images.
@@ -264,10 +285,3 @@ mediaViewer:
info: info:
enableEndUserAgreement: true enableEndUserAgreement: true
enablePrivacyStatement: true enablePrivacyStatement: true
# Home Page
homePage:
recentSubmissions:
# The number of item showing in recent submission components
pageSize: 5
# Sort record of recent submission
sortField: 'dc.date.accessioned'

View File

@@ -10,7 +10,7 @@ import {
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
ObservedValueOf, ObservedValueOf,
} from 'rxjs'; } from 'rxjs';
import { map, mergeMap, switchMap, take } from 'rxjs/operators'; import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model'; import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
@@ -144,7 +144,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
} }
}), }),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => { switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => { const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest( const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => { this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
@@ -153,8 +153,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
return epersonDtoModel; return epersonDtoModel;
}); });
return dto$; return dto$;
})); })]);
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => { return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
})); }));
})) }))
@@ -174,7 +174,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
return this.ePersonDataService.findListByHref(group._links.epersons.href, { return this.ePersonDataService.findListByHref(group._links.epersons.href, {
currentPage: 1, currentPage: 1,
elementsPerPage: 9999 elementsPerPage: 9999
}, false) })
.pipe( .pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
@@ -274,7 +274,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
} }
}), }),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => { switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => { const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest( const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => { this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
@@ -283,8 +283,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
return epersonDtoModel; return epersonDtoModel;
}); });
return dto$; return dto$;
})); })]);
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => { return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
})); }));
})) }))

View File

@@ -14,6 +14,14 @@ import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths'; import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
import { LinkService } from '../../../../../core/cache/builders/link.service'; import { LinkService } from '../../../../../core/cache/builders/link.service';
import { AuthService } from '../../../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
import { FileService } from '../../../../../core/shared/file.service';
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
describe('CollectionAdminSearchResultGridElementComponent', () => { describe('CollectionAdminSearchResultGridElementComponent', () => {
let component: CollectionAdminSearchResultGridElementComponent; let component: CollectionAdminSearchResultGridElementComponent;
@@ -45,7 +53,11 @@ describe('CollectionAdminSearchResultGridElementComponent', () => {
providers: [ providers: [
{ provide: TruncatableService, useValue: mockTruncatableService }, { provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: {} }, { provide: BitstreamDataService, useValue: {} },
{ provide: LinkService, useValue: linkService } { provide: LinkService, useValue: linkService },
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: FileService, useClass: FileServiceStub },
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
{ provide: ThemeService, useValue: getMockThemeService() },
] ]
}) })
.compileComponents(); .compileComponents();

View File

@@ -16,6 +16,14 @@ import { CommunitySearchResult } from '../../../../../shared/object-collection/s
import { Community } from '../../../../../core/shared/community.model'; import { Community } from '../../../../../core/shared/community.model';
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths'; import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
import { LinkService } from '../../../../../core/cache/builders/link.service'; import { LinkService } from '../../../../../core/cache/builders/link.service';
import { AuthService } from '../../../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
import { FileService } from '../../../../../core/shared/file.service';
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
describe('CommunityAdminSearchResultGridElementComponent', () => { describe('CommunityAdminSearchResultGridElementComponent', () => {
let component: CommunityAdminSearchResultGridElementComponent; let component: CommunityAdminSearchResultGridElementComponent;
@@ -47,7 +55,11 @@ describe('CommunityAdminSearchResultGridElementComponent', () => {
providers: [ providers: [
{ provide: TruncatableService, useValue: mockTruncatableService }, { provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: {} }, { provide: BitstreamDataService, useValue: {} },
{ provide: LinkService, useValue: linkService } { provide: LinkService, useValue: linkService },
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: FileService, useClass: FileServiceStub },
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
{ provide: ThemeService, useValue: getMockThemeService() },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })

View File

@@ -20,6 +20,12 @@ import { getMockThemeService } from '../../../../../shared/mocks/theme-service.m
import { ThemeService } from '../../../../../shared/theme-support/theme.service'; import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service'; import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model'; import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model';
import { AuthService } from '../../../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
import { FileService } from '../../../../../core/shared/file.service';
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
describe('ItemAdminSearchResultGridElementComponent', () => { describe('ItemAdminSearchResultGridElementComponent', () => {
let component: ItemAdminSearchResultGridElementComponent; let component: ItemAdminSearchResultGridElementComponent;
@@ -64,6 +70,9 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
{ provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: ThemeService, useValue: mockThemeService }, { provide: ThemeService, useValue: mockThemeService },
{ provide: AccessStatusDataService, useValue: mockAccessStatusDataService }, { provide: AccessStatusDataService, useValue: mockAccessStatusDataService },
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: FileService, useClass: FileServiceStub },
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })

View File

@@ -13,6 +13,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths'; import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
describe('CollectionAdminSearchResultListElementComponent', () => { describe('CollectionAdminSearchResultListElementComponent', () => {
let component: CollectionAdminSearchResultListElementComponent; let component: CollectionAdminSearchResultListElementComponent;
@@ -36,7 +38,8 @@ describe('CollectionAdminSearchResultListElementComponent', () => {
], ],
declarations: [CollectionAdminSearchResultListElementComponent], declarations: [CollectionAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} }, providers: [{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }], { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment }],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })
.compileComponents(); .compileComponents();

View File

@@ -13,6 +13,8 @@ import { Community } from '../../../../../core/shared/community.model';
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths'; import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
describe('CommunityAdminSearchResultListElementComponent', () => { describe('CommunityAdminSearchResultListElementComponent', () => {
let component: CommunityAdminSearchResultListElementComponent; let component: CommunityAdminSearchResultListElementComponent;
@@ -36,7 +38,8 @@ describe('CommunityAdminSearchResultListElementComponent', () => {
], ],
declarations: [CommunityAdminSearchResultListElementComponent], declarations: [CommunityAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} }, providers: [{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }], { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment }],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })
.compileComponents(); .compileComponents();

View File

@@ -10,6 +10,8 @@ import { ItemAdminSearchResultListElementComponent } from './item-admin-search-r
import { Item } from '../../../../../core/shared/item.model'; import { Item } from '../../../../../core/shared/item.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
describe('ItemAdminSearchResultListElementComponent', () => { describe('ItemAdminSearchResultListElementComponent', () => {
let component: ItemAdminSearchResultListElementComponent; let component: ItemAdminSearchResultListElementComponent;
@@ -33,7 +35,8 @@ describe('ItemAdminSearchResultListElementComponent', () => {
], ],
declarations: [ItemAdminSearchResultListElementComponent], declarations: [ItemAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} }, providers: [{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }], { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment }],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })
.compileComponents(); .compileComponents();

View File

@@ -18,6 +18,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
describe('WorkflowItemAdminWorkflowListElementComponent', () => { describe('WorkflowItemAdminWorkflowListElementComponent', () => {
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent; let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
@@ -51,7 +53,8 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => {
providers: [ providers: [
{ provide: TruncatableService, useValue: mockTruncatableService }, { provide: TruncatableService, useValue: mockTruncatableService },
{ provide: LinkService, useValue: linkService }, { provide: LinkService, useValue: linkService },
{ provide: DSONameService, useClass: DSONameServiceMock } { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model'; import { Context } from '../../../../../core/shared/context.model';
@@ -13,6 +13,7 @@ import { SearchResultListElementComponent } from '../../../../../shared/object-l
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) @listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
@Component({ @Component({
@@ -32,9 +33,10 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S
constructor(private linkService: LinkService, constructor(private linkService: LinkService,
protected truncatableService: TruncatableService, protected truncatableService: TruncatableService,
protected dsoNameService: DSONameService protected dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig: AppConfig
) { ) {
super(truncatableService, dsoNameService); super(truncatableService, dsoNameService, appConfig);
} }
/** /**

View File

@@ -18,11 +18,10 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-
import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec'; import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../core/data/find-list-options.model'; import { APP_CONFIG } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
describe('BrowseByDatePageComponent', () => { describe('BrowseByDatePageComponent', () => {
let comp: BrowseByDatePageComponent; let comp: BrowseByDatePageComponent;
@@ -83,7 +82,8 @@ describe('BrowseByDatePageComponent', () => {
{ provide: DSpaceObjectDataService, useValue: mockDsoService }, { provide: DSpaceObjectDataService, useValue: mockDsoService },
{ provide: Router, useValue: new RouterMock() }, { provide: Router, useValue: new RouterMock() },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: ChangeDetectorRef, useValue: mockCdRef } { provide: ChangeDetectorRef, useValue: mockCdRef },
{ provide: APP_CONFIG, useValue: environment }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -1,9 +1,8 @@
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { import {
BrowseByMetadataPageComponent, BrowseByMetadataPageComponent,
browseParamsToOptions browseParamsToOptions, getBrowseSearchOptions
} from '../browse-by-metadata-page/browse-by-metadata-page.component'; } from '../browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { combineLatest as observableCombineLatest } from 'rxjs'; import { combineLatest as observableCombineLatest } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
@@ -13,12 +12,12 @@ import { BrowseService } from '../../core/browse/browse.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { environment } from '../../../environments/environment';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { isValidDate } from '../../shared/date.util'; import { isValidDate } from '../../shared/date.util';
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
@Component({ @Component({
selector: 'ds-browse-by-date-page', selector: 'ds-browse-by-date-page',
@@ -43,14 +42,16 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
protected dsoService: DSpaceObjectDataService, protected dsoService: DSpaceObjectDataService,
protected router: Router, protected router: Router,
protected paginationService: PaginationService, protected paginationService: PaginationService,
protected cdRef: ChangeDetectorRef) { protected cdRef: ChangeDetectorRef,
super(route, browseService, dsoService, paginationService, router); @Inject(APP_CONFIG) public appConfig: AppConfig) {
super(route, browseService, dsoService, paginationService, router, appConfig);
} }
ngOnInit(): void { ngOnInit(): void {
const sortConfig = new SortOptions('default', SortDirection.ASC); const sortConfig = new SortOptions('default', SortDirection.ASC);
this.startsWithType = StartsWithType.date; this.startsWithType = StartsWithType.date;
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig)); // include the thumbnail configuration in browse search options
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push( this.subs.push(
@@ -63,7 +64,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys; const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
this.browseId = params.id || this.defaultBrowseId; this.browseId = params.id || this.defaultBrowseId;
this.startsWith = +params.startsWith || params.startsWith; this.startsWith = +params.startsWith || params.startsWith;
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails);
this.updatePageWithItems(searchOptions, this.value, undefined); this.updatePageWithItems(searchOptions, this.value, undefined);
this.updateParent(params.scope); this.updateParent(params.scope);
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope); this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
@@ -83,7 +84,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) { updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) {
this.subs.push( this.subs.push(
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => { this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
let lowerLimit = environment.browseBy.defaultLowerLimit; let lowerLimit = this.appConfig.browseBy.defaultLowerLimit;
if (hasValue(firstItemRD.payload)) { if (hasValue(firstItemRD.payload)) {
const date = firstItemRD.payload.firstMetadataValue(metadataKeys); const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
if (isNotEmpty(date) && isValidDate(date)) { if (isNotEmpty(date) && isValidDate(date)) {
@@ -94,8 +95,8 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
} }
const options = []; const options = [];
const currentYear = new Date().getUTCFullYear(); const currentYear = new Date().getUTCFullYear();
const oneYearBreak = Math.floor((currentYear - environment.browseBy.oneYearLimit) / 5) * 5; const oneYearBreak = Math.floor((currentYear - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
const fiveYearBreak = Math.floor((currentYear - environment.browseBy.fiveYearLimit) / 10) * 10; const fiveYearBreak = Math.floor((currentYear - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
if (lowerLimit <= fiveYearBreak) { if (lowerLimit <= fiveYearBreak) {
lowerLimit -= 10; lowerLimit -= 10;
} else if (lowerLimit <= oneYearBreak) { } else if (lowerLimit <= oneYearBreak) {

View File

@@ -1,4 +1,8 @@
import { BrowseByMetadataPageComponent, browseParamsToOptions } from './browse-by-metadata-page.component'; import {
BrowseByMetadataPageComponent,
browseParamsToOptions,
getBrowseSearchOptions
} from './browse-by-metadata-page.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { BrowseService } from '../../core/browse/browse.service'; import { BrowseService } from '../../core/browse/browse.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@@ -14,7 +18,7 @@ import { RemoteData } from '../../core/data/remote-data';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model'; import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
@@ -25,6 +29,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { APP_CONFIG } from '../../../config/app-config.interface';
describe('BrowseByMetadataPageComponent', () => { describe('BrowseByMetadataPageComponent', () => {
let comp: BrowseByMetadataPageComponent; let comp: BrowseByMetadataPageComponent;
@@ -43,6 +48,13 @@ describe('BrowseByMetadataPageComponent', () => {
] ]
}); });
const environmentMock = {
browseBy: {
showThumbnails: true,
pageSize: 10
}
};
const mockEntries = [ const mockEntries = [
{ {
type: BrowseEntry.type, type: BrowseEntry.type,
@@ -97,7 +109,8 @@ describe('BrowseByMetadataPageComponent', () => {
{ provide: BrowseService, useValue: mockBrowseService }, { provide: BrowseService, useValue: mockBrowseService },
{ provide: DSpaceObjectDataService, useValue: mockDsoService }, { provide: DSpaceObjectDataService, useValue: mockDsoService },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: Router, useValue: new RouterMock() } { provide: Router, useValue: new RouterMock() },
{ provide: APP_CONFIG, useValue: environmentMock }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -118,6 +131,10 @@ describe('BrowseByMetadataPageComponent', () => {
expect(comp.items$).toBeUndefined(); expect(comp.items$).toBeUndefined();
}); });
it('should set embed thumbnail property to true', () => {
expect(comp.fetchThumbnails).toBeTrue();
});
describe('when a value is provided', () => { describe('when a value is provided', () => {
beforeEach(() => { beforeEach(() => {
const paramsWithValue = { const paramsWithValue = {
@@ -145,14 +162,14 @@ describe('BrowseByMetadataPageComponent', () => {
}; };
const paginationOptions = Object.assign(new PaginationComponentOptions(), { const paginationOptions = Object.assign(new PaginationComponentOptions(), {
currentPage: 5, currentPage: 5,
pageSize: 10, pageSize: comp.appConfig.browseBy.pageSize,
}); });
const sortOptions = { const sortOptions = {
direction: SortDirection.ASC, direction: SortDirection.ASC,
field: 'fake-field', field: 'fake-field',
}; };
result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author'); result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author', comp.fetchThumbnails);
}); });
it('should return BrowseEntrySearchOptions with the correct properties', () => { it('should return BrowseEntrySearchOptions with the correct properties', () => {
@@ -163,6 +180,36 @@ describe('BrowseByMetadataPageComponent', () => {
expect(result.sort.direction).toEqual(SortDirection.ASC); expect(result.sort.direction).toEqual(SortDirection.ASC);
expect(result.sort.field).toEqual('fake-field'); expect(result.sort.field).toEqual('fake-field');
expect(result.scope).toEqual('fake-scope'); expect(result.scope).toEqual('fake-scope');
expect(result.fetchThumbnail).toBeTrue();
});
});
describe('calling getBrowseSearchOptions', () => {
let result: BrowseEntrySearchOptions;
beforeEach(() => {
const paramsScope = {
scope: 'fake-scope'
};
const paginationOptions = Object.assign(new PaginationComponentOptions(), {
currentPage: 5,
pageSize: comp.appConfig.browseBy.pageSize,
});
const sortOptions = {
direction: SortDirection.ASC,
field: 'fake-field',
};
result = getBrowseSearchOptions('title', paginationOptions, sortOptions, comp.fetchThumbnails);
});
it('should return BrowseEntrySearchOptions with the correct properties', () => {
expect(result.metadataDefinition).toEqual('title');
expect(result.pagination.currentPage).toEqual(5);
expect(result.pagination.pageSize).toEqual(10);
expect(result.sort.direction).toEqual(SortDirection.ASC);
expect(result.sort.field).toEqual('fake-field');
expect(result.fetchThumbnail).toBeTrue();
}); });
}); });
}); });

View File

@@ -1,5 +1,5 @@
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginatedList } from '../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
@@ -17,6 +17,7 @@ import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator'; import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
export const BBM_PAGINATION_ID = 'bbm'; export const BBM_PAGINATION_ID = 'bbm';
@@ -26,9 +27,10 @@ export const BBM_PAGINATION_ID = 'bbm';
templateUrl: './browse-by-metadata-page.component.html' templateUrl: './browse-by-metadata-page.component.html'
}) })
/** /**
* Component for browsing (items) by metadata definition * Component for browsing (items) by metadata definition.
* A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields. * A metadata definition (a.k.a. browse id) is a short term used to describe one
* An example would be 'author' for 'dc.contributor.*' * or multiple metadata fields. An example would be 'author' for
* 'dc.contributor.*'
*/ */
@rendersBrowseBy(BrowseByDataType.Metadata) @rendersBrowseBy(BrowseByDataType.Metadata)
export class BrowseByMetadataPageComponent implements OnInit { export class BrowseByMetadataPageComponent implements OnInit {
@@ -51,11 +53,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
/** /**
* The pagination config used to display the values * The pagination config used to display the values
*/ */
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { paginationConfig: PaginationComponentOptions;
id: BBM_PAGINATION_ID,
currentPage: 1,
pageSize: 20
});
/** /**
* The pagination observable * The pagination observable
@@ -111,16 +109,31 @@ export class BrowseByMetadataPageComponent implements OnInit {
*/ */
startsWith: string; startsWith: string;
/**
* Determines whether to request embedded thumbnail.
*/
fetchThumbnails: boolean;
public constructor(protected route: ActivatedRoute, public constructor(protected route: ActivatedRoute,
protected browseService: BrowseService, protected browseService: BrowseService,
protected dsoService: DSpaceObjectDataService, protected dsoService: DSpaceObjectDataService,
protected paginationService: PaginationService, protected paginationService: PaginationService,
protected router: Router) { protected router: Router,
} @Inject(APP_CONFIG) public appConfig: AppConfig) {
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
id: BBM_PAGINATION_ID,
currentPage: 1,
pageSize: this.appConfig.browseBy.pageSize,
});
}
ngOnInit(): void { ngOnInit(): void {
const sortConfig = new SortOptions('default', SortDirection.ASC); const sortConfig = new SortOptions('default', SortDirection.ASC);
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig)); this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push( this.subs.push(
@@ -133,15 +146,16 @@ export class BrowseByMetadataPageComponent implements OnInit {
this.authority = params.authority; this.authority = params.authority;
this.value = +params.value || params.value || ''; this.value = +params.value || params.value || '';
this.startsWith = +params.startsWith || params.startsWith; this.startsWith = +params.startsWith || params.startsWith;
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
if (isNotEmpty(this.value)) { if (isNotEmpty(this.value)) {
this.updatePageWithItems(searchOptions, this.value, this.authority); this.updatePageWithItems(
browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
} else { } else {
this.updatePage(searchOptions); this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false));
} }
this.updateParent(params.scope); this.updateParent(params.scope);
})); }));
this.updateStartsWithTextOptions(); this.updateStartsWithTextOptions();
} }
/** /**
@@ -228,22 +242,44 @@ export class BrowseByMetadataPageComponent implements OnInit {
} }
/**
* Creates browse entry search options.
* @param defaultBrowseId the metadata definition to fetch entries or items for
* @param paginationConfig the required pagination configuration
* @param sortConfig the required sort configuration
* @param fetchThumbnails optional boolean for fetching thumbnails
* @returns BrowseEntrySearchOptions instance
*/
export function getBrowseSearchOptions(defaultBrowseId: string,
paginationConfig: PaginationComponentOptions,
sortConfig: SortOptions,
fetchThumbnails?: boolean) {
if (!hasValue(fetchThumbnails)) {
fetchThumbnails = false;
}
return new BrowseEntrySearchOptions(defaultBrowseId, paginationConfig, sortConfig, null,
null, fetchThumbnails);
}
/** /**
* Function to transform query and url parameters into searchOptions used to fetch browse entries or items * Function to transform query and url parameters into searchOptions used to fetch browse entries or items
* @param params URL and query parameters * @param params URL and query parameters
* @param paginationConfig Pagination configuration * @param paginationConfig Pagination configuration
* @param sortConfig Sorting configuration * @param sortConfig Sorting configuration
* @param metadata Optional metadata definition to fetch browse entries/items for * @param metadata Optional metadata definition to fetch browse entries/items for
* @param fetchThumbnail Optional parameter for requesting thumbnail images
*/ */
export function browseParamsToOptions(params: any, export function browseParamsToOptions(params: any,
paginationConfig: PaginationComponentOptions, paginationConfig: PaginationComponentOptions,
sortConfig: SortOptions, sortConfig: SortOptions,
metadata?: string): BrowseEntrySearchOptions { metadata?: string,
fetchThumbnail?: boolean): BrowseEntrySearchOptions {
return new BrowseEntrySearchOptions( return new BrowseEntrySearchOptions(
metadata, metadata,
paginationConfig, paginationConfig,
sortConfig, sortConfig,
+params.startsWith || params.startsWith, +params.startsWith || params.startsWith,
params.scope params.scope,
fetchThumbnail
); );
} }

View File

@@ -18,11 +18,11 @@ import { BrowseService } from '../../core/browse/browse.service';
import { RouterMock } from '../../shared/mocks/router.mock'; import { RouterMock } from '../../shared/mocks/router.mock';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../core/data/find-list-options.model'; import { APP_CONFIG } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
describe('BrowseByTitlePageComponent', () => { describe('BrowseByTitlePageComponent', () => {
let comp: BrowseByTitlePageComponent; let comp: BrowseByTitlePageComponent;
@@ -77,7 +77,8 @@ describe('BrowseByTitlePageComponent', () => {
{ provide: BrowseService, useValue: mockBrowseService }, { provide: BrowseService, useValue: mockBrowseService },
{ provide: DSpaceObjectDataService, useValue: mockDsoService }, { provide: DSpaceObjectDataService, useValue: mockDsoService },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: Router, useValue: new RouterMock() } { provide: Router, useValue: new RouterMock() },
{ provide: APP_CONFIG, useValue: environment }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -1,12 +1,11 @@
import { combineLatest as observableCombineLatest } from 'rxjs'; import { combineLatest as observableCombineLatest } from 'rxjs';
import { Component } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { import {
BrowseByMetadataPageComponent, BrowseByMetadataPageComponent,
browseParamsToOptions browseParamsToOptions, getBrowseSearchOptions
} from '../browse-by-metadata-page/browse-by-metadata-page.component'; } from '../browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { BrowseService } from '../../core/browse/browse.service'; import { BrowseService } from '../../core/browse/browse.service';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
@@ -14,6 +13,7 @@ import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
@Component({ @Component({
selector: 'ds-browse-by-title-page', selector: 'ds-browse-by-title-page',
@@ -30,13 +30,15 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
protected browseService: BrowseService, protected browseService: BrowseService,
protected dsoService: DSpaceObjectDataService, protected dsoService: DSpaceObjectDataService,
protected paginationService: PaginationService, protected paginationService: PaginationService,
protected router: Router) { protected router: Router,
super(route, browseService, dsoService, paginationService, router); @Inject(APP_CONFIG) public appConfig: AppConfig) {
super(route, browseService, dsoService, paginationService, router, appConfig);
} }
ngOnInit(): void { ngOnInit(): void {
const sortConfig = new SortOptions('dc.title', SortDirection.ASC); const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.updatePage(new BrowseEntrySearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig)); // include the thumbnail configuration in browse search options
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push( this.subs.push(
@@ -47,7 +49,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
this.startsWith = +params.startsWith || params.startsWith; this.startsWith = +params.startsWith || params.startsWith;
this.browseId = params.id || this.defaultBrowseId; this.browseId = params.id || this.defaultBrowseId;
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined, undefined); this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
this.updateParent(params.scope); this.updateParent(params.scope);
})); }));
this.updateStartsWithTextOptions(); this.updateStartsWithTextOptions();

View File

@@ -28,6 +28,7 @@ import { AuthorizationDataService } from '../core/data/feature-authorization/aut
import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { getCollectionPageRoute } from './collection-page-routing-paths'; import { getCollectionPageRoute } from './collection-page-routing-paths';
import { redirectOn4xx } from '../core/shared/authorized.operators'; import { redirectOn4xx } from '../core/shared/authorized.operators';
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
@Component({ @Component({
selector: 'ds-collection-page', selector: 'ds-collection-page',
@@ -74,6 +75,7 @@ export class CollectionPageComponent implements OnInit {
this.paginationConfig.pageSize = 5; this.paginationConfig.pageSize = 5;
this.paginationConfig.currentPage = 1; this.paginationConfig.currentPage = 1;
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC); this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
} }
ngOnInit(): void { ngOnInit(): void {
@@ -102,13 +104,14 @@ export class CollectionPageComponent implements OnInit {
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
map((rd) => rd.payload.id), map((rd) => rd.payload.id),
switchMap((id: string) => { switchMap((id: string) => {
return this.searchService.search( return this.searchService.search<Item>(
new PaginatedSearchOptions({ new PaginatedSearchOptions({
scope: id, scope: id,
pagination: currentPagination, pagination: currentPagination,
sort: currentSort, sort: currentSort,
dsoTypes: [DSpaceObjectType.ITEM] dsoTypes: [DSpaceObjectType.ITEM]
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>; }), null, true, true, ...BROWSE_LINKS_TO_FOLLOW)
.pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
}), }),
startWith(undefined) // Make sure switching pages shows loading component startWith(undefined) // Make sure switching pages shows loading component
) )

View File

@@ -17,7 +17,7 @@ export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
followLink('parentCommunity', {}, followLink('parentCommunity', {},
followLink('parentCommunity') followLink('parentCommunity')
), ),
followLink('logo') followLink('logo'),
]; ];
/** /**

View File

@@ -15,6 +15,8 @@ import { Collection } from '../core/shared/collection.model';
import { PageInfo } from '../core/shared/page-info.model'; import { PageInfo } from '../core/shared/page-info.model';
import { FlatNode } from './flat-node.model'; import { FlatNode } from './flat-node.model';
import { FindListOptions } from '../core/data/find-list-options.model'; import { FindListOptions } from '../core/data/find-list-options.model';
import { APP_CONFIG } from 'src/config/app-config.interface';
import { environment } from 'src/environments/environment.test';
describe('CommunityListService', () => { describe('CommunityListService', () => {
let store: StoreMock<AppState>; let store: StoreMock<AppState>;
@@ -191,13 +193,14 @@ describe('CommunityListService', () => {
}; };
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [CommunityListService, providers: [CommunityListService,
{ provide: APP_CONFIG, useValue: environment },
{ provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: CollectionDataService, useValue: collectionDataServiceStub },
{ provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: Store, useValue: StoreMock }, { provide: Store, useValue: StoreMock },
], ],
}); });
store = TestBed.inject(Store as any); store = TestBed.inject(Store as any);
service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store); service = new CommunityListService(environment, communityDataServiceStub, collectionDataServiceStub, store);
}); });
it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => { it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => {

View File

@@ -1,5 +1,5 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { createSelector, Store } from '@ngrx/store'; import { createSelector, Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
@@ -23,6 +23,7 @@ import { followLink } from '../shared/utils/follow-link-config.model';
import { FlatNode } from './flat-node.model'; import { FlatNode } from './flat-node.model';
import { ShowMoreFlatNode } from './show-more-flat-node.model'; import { ShowMoreFlatNode } from './show-more-flat-node.model';
import { FindListOptions } from '../core/data/find-list-options.model'; import { FindListOptions } from '../core/data/find-list-options.model';
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
// Helper method to combine an flatten an array of observables of flatNode arrays // Helper method to combine an flatten an array of observables of flatNode arrays
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> => export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
@@ -80,8 +81,6 @@ const communityListStateSelector = (state: AppState) => state.communityList;
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes); const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode); const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
export const MAX_COMCOLS_PER_PAGE = 20;
/** /**
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource * Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
* and connection to the store to retrieve and save the state of the community list * and connection to the store to retrieve and save the state of the community list
@@ -89,8 +88,15 @@ export const MAX_COMCOLS_PER_PAGE = 20;
@Injectable() @Injectable()
export class CommunityListService { export class CommunityListService {
constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService, private pageSize: number;
private store: Store<any>) {
constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
private communityDataService: CommunityDataService,
private collectionDataService: CollectionDataService,
private store: Store<any>
) {
this.pageSize = appConfig.communityList.pageSize;
} }
private configOnePage: FindListOptions = Object.assign(new FindListOptions(), { private configOnePage: FindListOptions = Object.assign(new FindListOptions(), {
@@ -145,7 +151,7 @@ export class CommunityListService {
private getTopCommunities(options: FindListOptions): Observable<PaginatedList<Community>> { private getTopCommunities(options: FindListOptions): Observable<PaginatedList<Community>> {
return this.communityDataService.findTop({ return this.communityDataService.findTop({
currentPage: options.currentPage, currentPage: options.currentPage,
elementsPerPage: MAX_COMCOLS_PER_PAGE, elementsPerPage: this.pageSize,
sort: { sort: {
field: options.sort.field, field: options.sort.field,
direction: options.sort.direction direction: options.sort.direction
@@ -216,7 +222,7 @@ export class CommunityListService {
let subcoms = []; let subcoms = [];
for (let i = 1; i <= currentCommunityPage; i++) { for (let i = 1; i <= currentCommunityPage; i++) {
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, { const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
elementsPerPage: MAX_COMCOLS_PER_PAGE, elementsPerPage: this.pageSize,
currentPage: i currentPage: i
}, },
followLink('subcommunities', { findListOptions: this.configOnePage }), followLink('subcommunities', { findListOptions: this.configOnePage }),
@@ -241,7 +247,7 @@ export class CommunityListService {
let collections = []; let collections = [];
for (let i = 1; i <= currentCollectionPage; i++) { for (let i = 1; i <= currentCollectionPage; i++) {
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, { const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
elementsPerPage: MAX_COMCOLS_PER_PAGE, elementsPerPage: this.pageSize,
currentPage: i currentPage: i
}) })
.pipe( .pipe(

View File

@@ -25,12 +25,13 @@
</div> </div>
</div> </div>
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section">
<!-- Browse-By Links --> <!-- Browse-By Links -->
<ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type"> <ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
</ds-themed-comcol-page-browse-by> </ds-themed-comcol-page-browse-by>
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list> <ds-themed-community-page-sub-community-list [community]="communityPayload"></ds-themed-community-page-sub-community-list>
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list> <ds-themed-community-page-sub-collection-list [community]="communityPayload"></ds-themed-community-page-sub-collection-list>
</section> </section>
<footer *ngIf="communityPayload.copyrightText" class="border-top my-5 pt-4"> <footer *ngIf="communityPayload.copyrightText" class="border-top my-5 pt-4">
<!-- Copyright --> <!-- Copyright -->

View File

@@ -13,10 +13,18 @@ import { StatisticsModule } from '../statistics/statistics.module';
import { CommunityFormModule } from './community-form/community-form.module'; import { CommunityFormModule } from './community-form/community-form.module';
import { ThemedCommunityPageComponent } from './themed-community-page.component'; import { ThemedCommunityPageComponent } from './themed-community-page.component';
import { ComcolModule } from '../shared/comcol/comcol.module'; import { ComcolModule } from '../shared/comcol/comcol.module';
import {
ThemedCommunityPageSubCommunityListComponent
} from './sub-community-list/themed-community-page-sub-community-list.component';
import {
ThemedCollectionPageSubCollectionListComponent
} from './sub-collection-list/themed-community-page-sub-collection-list.component';
const DECLARATIONS = [CommunityPageComponent, const DECLARATIONS = [CommunityPageComponent,
ThemedCommunityPageComponent, ThemedCommunityPageComponent,
ThemedCommunityPageSubCommunityListComponent,
CommunityPageSubCollectionListComponent, CommunityPageSubCollectionListComponent,
ThemedCollectionPageSubCollectionListComponent,
CommunityPageSubCommunityListComponent, CommunityPageSubCommunityListComponent,
CreateCommunityPageComponent, CreateCommunityPageComponent,
DeleteCommunityPageComponent]; DeleteCommunityPageComponent];

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs'; import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
@@ -12,6 +12,7 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util';
@Component({ @Component({
selector: 'ds-community-page-sub-collection-list', selector: 'ds-community-page-sub-collection-list',
@@ -19,9 +20,15 @@ import { switchMap } from 'rxjs/operators';
templateUrl: './community-page-sub-collection-list.component.html', templateUrl: './community-page-sub-collection-list.component.html',
animations:[fadeIn] animations:[fadeIn]
}) })
export class CommunityPageSubCollectionListComponent implements OnInit { export class CommunityPageSubCollectionListComponent implements OnInit, OnDestroy {
@Input() community: Community; @Input() community: Community;
/**
* Optional page size. Overrides communityList.pageSize configuration for this component.
* Value can be added in the themed version of the parent component.
*/
@Input() pageSize: number;
/** /**
* The pagination configuration * The pagination configuration
*/ */
@@ -50,7 +57,9 @@ export class CommunityPageSubCollectionListComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.config = new PaginationComponentOptions(); this.config = new PaginationComponentOptions();
this.config.id = this.pageId; this.config.id = this.pageId;
this.config.pageSize = 5; if (hasValue(this.pageSize)) {
this.config.pageSize = this.pageSize;
}
this.config.currentPage = 1; this.config.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.initPage(); this.initPage();

View File

@@ -0,0 +1,28 @@
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component';
import { Component, Input } from '@angular/core';
import { Community } from '../../core/shared/community.model';
@Component({
selector: 'ds-themed-community-page-sub-collection-list',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent<CommunityPageSubCollectionListComponent> {
@Input() community: Community;
@Input() pageSize: number;
protected inAndOutputNames: (keyof CommunityPageSubCollectionListComponent & keyof this)[] = ['community', 'pageSize'];
protected getComponentName(): string {
return 'CommunityPageSubCollectionListComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/community-page/sub-collection-list/community-page-sub-collection-list.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./community-page-sub-collection-list.component`);
}
}

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs'; import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
@@ -12,6 +12,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { takeUntilCompletedRemoteData } from '../../core/shared/operators'; import { takeUntilCompletedRemoteData } from '../../core/shared/operators';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { hasValue } from '../../shared/empty.util';
@Component({ @Component({
selector: 'ds-community-page-sub-community-list', selector: 'ds-community-page-sub-community-list',
@@ -22,9 +23,15 @@ import { PaginationService } from '../../core/pagination/pagination.service';
/** /**
* Component to render the sub-communities of a Community * Component to render the sub-communities of a Community
*/ */
export class CommunityPageSubCommunityListComponent implements OnInit { export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy {
@Input() community: Community; @Input() community: Community;
/**
* Optional page size. Overrides communityList.pageSize configuration for this component.
* Value can be added in the themed version of the parent component.
*/
@Input() pageSize: number;
/** /**
* The pagination configuration * The pagination configuration
*/ */
@@ -53,7 +60,9 @@ export class CommunityPageSubCommunityListComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.config = new PaginationComponentOptions(); this.config = new PaginationComponentOptions();
this.config.id = this.pageId; this.config.id = this.pageId;
this.config.pageSize = 5; if (hasValue(this.pageSize)) {
this.config.pageSize = this.pageSize;
}
this.config.currentPage = 1; this.config.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.initPage(); this.initPage();

View File

@@ -0,0 +1,29 @@
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component';
import { Component, Input } from '@angular/core';
import { Community } from '../../core/shared/community.model';
@Component({
selector: 'ds-themed-community-page-sub-community-list',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent<CommunityPageSubCommunityListComponent> {
@Input() community: Community;
@Input() pageSize: number;
protected inAndOutputNames: (keyof CommunityPageSubCommunityListComponent & keyof this)[] = ['community', 'pageSize'];
protected getComponentName(): string {
return 'CommunityPageSubCommunityListComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/community-page/sub-community-list/community-page-sub-community-list.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./community-page-sub-community-list.component`);
}
}

View File

@@ -111,7 +111,6 @@ describe(`AuthRequestService`, () => {
body: undefined, body: undefined,
options, options,
})); }));
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
}); });
}); });
}); });
@@ -151,7 +150,6 @@ describe(`AuthRequestService`, () => {
body: { content: 'something' }, body: { content: 'something' },
options, options,
})); }));
expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID);
}); });
}); });
}); });

View File

@@ -58,7 +58,9 @@ export abstract class AuthRequestService {
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> { public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<RemoteData<AuthStatus>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
this.halService.getEndpoint(this.linkName).pipe( const endpoint$ = this.halService.getEndpoint(this.linkName);
endpoint$.pipe(
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
distinctUntilChanged(), distinctUntilChanged(),
@@ -68,7 +70,9 @@ export abstract class AuthRequestService {
this.requestService.send(request); this.requestService.send(request);
}); });
return this.fetchRequest(requestId); return endpoint$.pipe(
switchMap(() => this.fetchRequest(requestId)),
);
} }
/** /**
@@ -79,7 +83,9 @@ export abstract class AuthRequestService {
public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> { public getRequest(method: string, options?: HttpOptions, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<AuthStatus>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
this.halService.getEndpoint(this.linkName).pipe( const endpoint$ = this.halService.getEndpoint(this.linkName);
endpoint$.pipe(
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),
map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)),
distinctUntilChanged(), distinctUntilChanged(),
@@ -89,7 +95,9 @@ export abstract class AuthRequestService {
this.requestService.send(request); this.requestService.send(request);
}); });
return this.fetchRequest(requestId, ...linksToFollow); return endpoint$.pipe(
switchMap(() => this.fetchRequest(requestId, ...linksToFollow)),
);
} }
/** /**
* Factory function to create the request object to send. This needs to be a POST client side and * Factory function to create the request object to send. This needs to be a POST client side and

View File

@@ -31,6 +31,8 @@ export class DSONameService {
const givenName = dso.firstMetadataValue('person.givenName'); const givenName = dso.firstMetadataValue('person.givenName');
if (isEmpty(familyName) && isEmpty(givenName)) { if (isEmpty(familyName) && isEmpty(givenName)) {
return dso.firstMetadataValue('dc.title') || dso.name; return dso.firstMetadataValue('dc.title') || dso.name;
} else if (isEmpty(familyName) || isEmpty(givenName)) {
return familyName || givenName;
} else { } else {
return `${familyName}, ${givenName}`; return `${familyName}, ${givenName}`;
} }
@@ -55,11 +57,14 @@ export class DSONameService {
.filter((type) => typeof type === 'string') .filter((type) => typeof type === 'string')
.find((type: string) => Object.keys(this.factories).includes(type)) as string; .find((type: string) => Object.keys(this.factories).includes(type)) as string;
let name;
if (hasValue(match)) { if (hasValue(match)) {
return this.factories[match](dso); name = this.factories[match](dso);
} else {
return this.factories.Default(dso);
} }
if (isEmpty(name)) {
name = this.factories.Default(dso);
}
return name;
} }
} }

View File

@@ -6,13 +6,16 @@ import { SortOptions } from '../cache/models/sort-options.model';
* - metadataDefinition: The metadata definition to fetch entries or items for * - metadataDefinition: The metadata definition to fetch entries or items for
* - pagination: Optional pagination options to use * - pagination: Optional pagination options to use
* - sort: Optional sorting options to use * - sort: Optional sorting options to use
* - startsWith An optional value to use to filter the browse results
* - scope: An optional scope to limit the results within a specific collection or community * - scope: An optional scope to limit the results within a specific collection or community
* - fetchThumbnail An optional boolean to request thumbnail for items
*/ */
export class BrowseEntrySearchOptions { export class BrowseEntrySearchOptions {
constructor(public metadataDefinition: string, constructor(public metadataDefinition: string,
public pagination?: PaginationComponentOptions, public pagination?: PaginationComponentOptions,
public sort?: SortOptions, public sort?: SortOptions,
public startsWith?: string, public startsWith?: string,
public scope?: string) { public scope?: string,
public fetchThumbnail?: boolean) {
} }
} }

View File

@@ -21,6 +21,12 @@ import { URLCombiner } from '../url-combiner/url-combiner';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { BrowseDefinitionDataService } from './browse-definition-data.service'; import { BrowseDefinitionDataService } from './browse-definition-data.service';
import { HrefOnlyDataService } from '../data/href-only-data.service'; import { HrefOnlyDataService } from '../data/href-only-data.service';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
followLink('thumbnail')
];
/** /**
* The service handling all browse requests * The service handling all browse requests
@@ -96,6 +102,9 @@ export class BrowseService {
return href; return href;
}) })
); );
if (options.fetchThumbnail ) {
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
}
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$); return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$);
} }
@@ -141,6 +150,9 @@ export class BrowseService {
return href; return href;
}), }),
); );
if (options.fetchThumbnail) {
return this.hrefOnlyDataService.findListByHref<Item>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
}
return this.hrefOnlyDataService.findListByHref<Item>(href$); return this.hrefOnlyDataService.findListByHref<Item>(href$);
} }

View File

@@ -1,4 +1,4 @@
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model';
import { Item } from '../../shared/item.model'; import { Item } from '../../shared/item.model';
import { PageInfo } from '../../shared/page-info.model'; import { PageInfo } from '../../shared/page-info.model';
@@ -18,6 +18,9 @@ import { take } from 'rxjs/operators';
import { HALLink } from '../../shared/hal-link.model'; import { HALLink } from '../../shared/hal-link.model';
import { RequestEntryState } from '../../data/request-entry-state.model'; import { RequestEntryState } from '../../data/request-entry-state.model';
import { RequestEntry } from '../../data/request-entry.model'; import { RequestEntry } from '../../data/request-entry.model';
import { cold } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { fakeAsync, tick } from '@angular/core/testing';
describe('RemoteDataBuildService', () => { describe('RemoteDataBuildService', () => {
let service: RemoteDataBuildService; let service: RemoteDataBuildService;
@@ -646,4 +649,211 @@ describe('RemoteDataBuildService', () => {
}); });
}); });
}); });
describe('buildFromHref', () => {
beforeEach(() => {
(objectCache.getRequestUUIDBySelfLink as jasmine.Spy).and.returnValue(cold('a', { a: 'request/uuid' }));
});
describe('when both getRequestFromRequestHref and getRequestFromRequestUUID emit nothing', () => {
beforeEach(() => {
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: undefined }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: undefined }));
});
it('should not emit anything', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold(''));
});
});
describe('when one of getRequestFromRequestHref or getRequestFromRequestUUID emits nothing', () => {
let requestEntry: RequestEntry;
beforeEach(() => {
requestEntry = Object.assign(new RequestEntry(), {
state: RequestEntryState.Success,
request: {},
});
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: undefined }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry }));
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
});
it('should create remote-data with the existing request-entry', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
a: new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, {}, undefined),
}));
});
});
describe('when one of getRequestFromRequestHref or getRequestFromRequestUUID is stale', () => {
let requestEntry1: RequestEntry;
let requestEntry2: RequestEntry;
beforeEach(() => {
requestEntry1 = Object.assign(new RequestEntry(), {
state: RequestEntryState.Success,
request: {},
});
requestEntry2 = Object.assign(new RequestEntry(), {
state: RequestEntryState.SuccessStale,
request: {},
});
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry1 }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry2 }));
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
});
it('should create remote-data with the non-stale request-entry', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
a: new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, {}, undefined),
}));
});
});
describe('when both getRequestFromRequestHref and getRequestFromRequestUUID are stale', () => {
let requestEntry1: RequestEntry;
let requestEntry2: RequestEntry;
beforeEach(() => {
requestEntry1 = Object.assign(new RequestEntry(), {
state: RequestEntryState.SuccessStale,
request: {},
lastUpdated: 20,
});
requestEntry2 = Object.assign(new RequestEntry(), {
state: RequestEntryState.SuccessStale,
request: {},
lastUpdated: 10,
});
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry1 }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry2 }));
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
});
it('should create remote-data with the most up-to-date request-entry', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
a: new RemoteData(undefined, undefined, 20, RequestEntryState.SuccessStale, undefined, {}, undefined),
}));
});
});
describe('when both getRequestFromRequestHref and getRequestFromRequestUUID are not stale', () => {
let requestEntry1: RequestEntry;
let requestEntry2: RequestEntry;
beforeEach(() => {
requestEntry1 = Object.assign(new RequestEntry(), {
state: RequestEntryState.Success,
request: {},
lastUpdated: 25,
});
requestEntry2 = Object.assign(new RequestEntry(), {
state: RequestEntryState.Success,
request: {},
lastUpdated: 5,
});
(requestService.getByHref as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry1 }));
(requestService.getByUUID as jasmine.Spy).and.returnValue(cold('a', { a: requestEntry2 }));
spyOn((service as any), 'buildPayload').and.returnValue(cold('a', { a: {} }));
});
it('should create remote-data with the most up-to-date request-entry', () => {
expect(service.buildFromHref(cold('a', { a: 'rest/api/endpoint' }))).toBeObservable(cold('a', {
a: new RemoteData(undefined, undefined, 25, RequestEntryState.Success, undefined, {}, undefined),
}));
});
});
});
describe('buildFromRequestUUIDAndAwait', () => {
let testScheduler;
let callback: jasmine.Spy;
let buildFromRequestUUIDSpy;
const BOOLEAN = { t: true, f: false };
const MOCK_PENDING_RD = createPendingRemoteDataObject();
const MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
const MOCK_FAILED_RD = createFailedRemoteDataObject('failed');
const RDs = {
p: MOCK_PENDING_RD,
s: MOCK_SUCCEEDED_RD,
f: MOCK_FAILED_RD,
};
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
callback = jasmine.createSpy('callback');
callback.and.returnValue(observableOf(undefined));
buildFromRequestUUIDSpy = spyOn(service, 'buildFromRequestUUID').and.callThrough();
});
it('should patch through href & followLinks to buildFromRequestUUID', () => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback, ...linksToFollow);
expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith('some-href', ...linksToFollow);
});
it('should trigger the callback on successful RD', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
expect(rd).toBe(MOCK_SUCCEEDED_RD);
expect(callback).toHaveBeenCalled();
done();
});
});
it('should trigger the callback on successful RD even if nothing subscribes to the returned Observable', fakeAsync(() => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback);
tick();
expect(callback).toHaveBeenCalled();
}));
it('should not trigger the callback on pending RD', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_PENDING_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
expect(rd).toBe(MOCK_PENDING_RD);
expect(callback).not.toHaveBeenCalled();
done();
});
});
it('should not trigger the callback on failed RD', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
expect(rd).toBe(MOCK_FAILED_RD);
expect(callback).not.toHaveBeenCalled();
done();
});
});
it('should only emit after the callback is done', () => {
testScheduler.run(({ cold: tsCold, expectObservable }) => {
buildFromRequestUUIDSpy.and.returnValue(
tsCold('-p----s', RDs)
);
callback.and.returnValue(
tsCold(' --t', BOOLEAN)
);
const done$ = service.buildFromRequestUUIDAndAwait('some-href', callback);
expectObservable(done$).toBe(
' -p------s', RDs // resulting duration between pending & successful includes the callback
);
});
});
});
}); });

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
AsyncSubject,
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
Observable, Observable,
of as observableOf, of as observableOf,
race as observableRace
} from 'rxjs'; } from 'rxjs';
import { map, switchMap, filter, distinctUntilKeyChanged } from 'rxjs/operators'; import { map, switchMap, filter, distinctUntilKeyChanged, startWith } from 'rxjs/operators';
import { hasValue, isEmpty, isNotEmpty, hasNoValue, isUndefined } from '../../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty, hasNoValue, isUndefined } from '../../../shared/empty.util';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model'; import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model';
@@ -21,10 +21,11 @@ import { HALResource } from '../../shared/hal-resource.model';
import { PAGINATED_LIST } from '../../data/paginated-list.resource-type'; import { PAGINATED_LIST } from '../../data/paginated-list.resource-type';
import { getUrlWithoutEmbedParams } from '../../index/index.selectors'; import { getUrlWithoutEmbedParams } from '../../index/index.selectors';
import { getResourceTypeValueFor } from '../object-cache.reducer'; import { getResourceTypeValueFor } from '../object-cache.reducer';
import { hasSucceeded, RequestEntryState } from '../../data/request-entry-state.model'; import { hasSucceeded, isStale, RequestEntryState } from '../../data/request-entry-state.model';
import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators'; import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators';
import { RequestEntry } from '../../data/request-entry.model'; import { RequestEntry } from '../../data/request-entry.model';
import { ResponseState } from '../../data/response-state.model'; import { ResponseState } from '../../data/response-state.model';
import { getFirstCompletedRemoteData } from '../../shared/operators';
@Injectable() @Injectable()
export class RemoteDataBuildService { export class RemoteDataBuildService {
@@ -189,6 +190,49 @@ export class RemoteDataBuildService {
return this.toRemoteDataObservable<T>(requestEntry$, payload$); return this.toRemoteDataObservable<T>(requestEntry$, payload$);
} }
/**
* Creates a {@link RemoteData} object for a rest request and its response
* and emits it only after the callback function is completed.
*
* @param requestUUID$ The UUID of the request we want to retrieve
* @param callback A function that returns an Observable. It will only be called once the request has succeeded.
* Then, the response will only be emitted after this callback function has emitted.
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
buildFromRequestUUIDAndAwait<T>(requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<T>) => Observable<unknown>, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<T>> {
const response$ = this.buildFromRequestUUID(requestUUID$, ...linksToFollow);
const callbackDone$ = new AsyncSubject<boolean>();
response$.pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<any>) => {
if (rd.hasSucceeded) {
// if the request succeeded, execute the callback
return callback(rd);
} else {
// otherwise, emit right away so the subscription doesn't stick around
return [true];
}
}),
).subscribe(() => {
callbackDone$.next(true);
callbackDone$.complete();
});
return response$.pipe(
switchMap((rd: RemoteData<any>) => {
if (rd.hasSucceeded) {
// if the request succeeded, wait for the callback to finish
return callbackDone$.pipe(
map(() => rd),
);
} else {
return [rd];
}
})
);
}
/** /**
* Creates a {@link RemoteData} object for a rest request and its response * Creates a {@link RemoteData} object for a rest request and its response
* *
@@ -207,10 +251,27 @@ export class RemoteDataBuildService {
this.objectCache.getRequestUUIDBySelfLink(href)), this.objectCache.getRequestUUIDBySelfLink(href)),
); );
const requestEntry$ = observableRace( const requestEntry$ = observableCombineLatest([
href$.pipe(getRequestFromRequestHref(this.requestService)), href$.pipe(getRequestFromRequestHref(this.requestService), startWith(undefined)),
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)), requestUUID$.pipe(getRequestFromRequestUUID(this.requestService), startWith(undefined)),
).pipe( ]).pipe(
filter(([r1, r2]) => hasValue(r1) || hasValue(r2)),
map(([r1, r2]) => {
// If one of the two requests has no value, return the other (both is impossible due to the filter above)
if (hasNoValue(r2)) {
return r1;
} else if (hasNoValue(r1)) {
return r2;
}
if ((isStale(r1.state) && isStale(r2.state)) || (!isStale(r1.state) && !isStale(r2.state))) {
// Neither or both are stale, pick the most recent request
return r1.lastUpdated >= r2.lastUpdated ? r1 : r2;
} else {
// One of the two is stale, return the not stale request
return isStale(r2.state) ? r1 : r2;
}
}),
distinctUntilKeyChanged('lastUpdated') distinctUntilKeyChanged('lastUpdated')
); );

View File

@@ -22,7 +22,7 @@ import { RequestEntryState } from '../request-entry-state.model';
import { DeleteData, DeleteDataImpl } from './delete-data'; import { DeleteData, DeleteDataImpl } from './delete-data';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { fakeAsync, tick } from '@angular/core/testing'; import { RestRequestMethod } from '../rest-request-method';
/** /**
* Tests whether calls to `DeleteData` methods are correctly patched through in a concrete data service that implements it * Tests whether calls to `DeleteData` methods are correctly patched through in a concrete data service that implements it
@@ -63,8 +63,6 @@ export function testDeleteDataImplementation(serviceFactory: () => DeleteData<an
const endpoint = 'https://rest.api/core'; const endpoint = 'https://rest.api/core';
const BOOLEAN = { f: false, t: true };
class TestService extends DeleteDataImpl<any> { class TestService extends DeleteDataImpl<any> {
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
@@ -155,13 +153,13 @@ describe('DeleteDataImpl', () => {
let MOCK_FAILED_RD; let MOCK_FAILED_RD;
let invalidateByHrefSpy: jasmine.Spy; let invalidateByHrefSpy: jasmine.Spy;
let buildFromRequestUUIDSpy: jasmine.Spy; let buildFromRequestUUIDAndAwaitSpy: jasmine.Spy;
let getIDHrefObsSpy: jasmine.Spy; let getIDHrefObsSpy: jasmine.Spy;
let deleteByHrefSpy: jasmine.Spy; let deleteByHrefSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true)); invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough(); buildFromRequestUUIDAndAwaitSpy = spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();
getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough(); getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough();
deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough(); deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough();
@@ -171,7 +169,7 @@ describe('DeleteDataImpl', () => {
it('should retrieve href by ID and call deleteByHref', () => { it('should retrieve href by ID and call deleteByHref', () => {
getIDHrefObsSpy.and.returnValue(observableOf('some-href')); getIDHrefObsSpy.and.returnValue(observableOf('some-href'));
buildFromRequestUUIDSpy.and.returnValue(createSuccessfulRemoteDataObject$({})); buildFromRequestUUIDAndAwaitSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => { service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => {
expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id'); expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id');
@@ -180,66 +178,53 @@ describe('DeleteDataImpl', () => {
}); });
describe('deleteByHref', () => { describe('deleteByHref', () => {
it('should call invalidateByHref if the DELETE request succeeds', (done) => { it('should send a DELETE request', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD)); buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.deleteByHref('some-href').subscribe(() => {
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
method: RestRequestMethod.DELETE,
href: 'some-href',
}));
done();
});
});
it('should include the virtual metadata to be copied in the DELETE request', (done) => {
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.deleteByHref('some-href', ['a', 'b', 'c']).subscribe(() => {
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
method: RestRequestMethod.DELETE,
href: 'some-href?copyVirtualMetadata=a&copyVirtualMetadata=b&copyVirtualMetadata=c',
}));
done();
});
});
it('should invalidate the currently cached object', (done) => {
service.deleteByHref('some-href').subscribe(() => {
expect(buildFromRequestUUIDAndAwaitSpy).toHaveBeenCalledWith(
requestService.generateRequestId(),
jasmine.anything(),
);
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
callback();
expect(service.invalidateByHref).toHaveBeenCalledWith('some-href');
done();
});
});
it('should return the RemoteData of the response', (done) => {
buildFromRequestUUIDAndAwaitSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.deleteByHref('some-href').subscribe(rd => { service.deleteByHref('some-href').subscribe(rd => {
expect(rd).toBe(MOCK_SUCCEEDED_RD); expect(rd).toBe(MOCK_SUCCEEDED_RD);
expect(invalidateByHrefSpy).toHaveBeenCalled();
done(); done();
}); });
}); });
it('should call invalidateByHref even if not subscribing to returned Observable', fakeAsync(() => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.deleteByHref('some-href');
tick();
expect(invalidateByHrefSpy).toHaveBeenCalled();
}));
it('should not call invalidateByHref if the DELETE request fails', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
service.deleteByHref('some-href').subscribe(rd => {
expect(rd).toBe(MOCK_FAILED_RD);
expect(invalidateByHrefSpy).not.toHaveBeenCalled();
done();
});
});
it('should wait for invalidateByHref before emitting', () => {
testScheduler.run(({ cold, expectObservable }) => {
buildFromRequestUUIDSpy.and.returnValue(
cold('(r|)', { r: MOCK_SUCCEEDED_RD}) // RD emits right away
);
invalidateByHrefSpy.and.returnValue(
cold('----(t|)', BOOLEAN) // but we pretend that setting requests to stale takes longer
);
const done$ = service.deleteByHref('some-href');
expectObservable(done$).toBe(
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait until that's done
);
});
});
it('should wait for the DELETE request to resolve before emitting', () => {
testScheduler.run(({ cold, expectObservable }) => {
buildFromRequestUUIDSpy.and.returnValue(
cold('----(r|)', { r: MOCK_SUCCEEDED_RD}) // the request takes a while
);
invalidateByHrefSpy.and.returnValue(
cold('(t|)', BOOLEAN) // but we pretend that setting to stale happens sooner
); // e.g.: maybe already stale before this call?
const done$ = service.deleteByHref('some-href');
expectObservable(done$).toBe(
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait for the request
);
});
});
}); });
}); });
}); });

View File

@@ -6,13 +6,12 @@
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
import { CacheableObject } from '../../cache/cacheable-object.model'; import { CacheableObject } from '../../cache/cacheable-object.model';
import { AsyncSubject, combineLatest, Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { RemoteData } from '../remote-data'; import { RemoteData } from '../remote-data';
import { NoContent } from '../../shared/NoContent.model'; import { NoContent } from '../../shared/NoContent.model';
import { filter, map, switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { DeleteRequest } from '../request.models'; import { DeleteRequest } from '../request.models';
import { hasNoValue, hasValue } from '../../../shared/empty.util'; import { hasNoValue, hasValue } from '../../../shared/empty.util';
import { getFirstCompletedRemoteData } from '../../shared/operators';
import { RequestService } from '../request.service'; import { RequestService } from '../request.service';
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service';
@@ -83,26 +82,6 @@ export class DeleteDataImpl<T extends CacheableObject> extends IdentifiableDataS
} }
this.requestService.send(request); this.requestService.send(request);
const response$ = this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(href));
const invalidated$ = new AsyncSubject<boolean>();
response$.pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) {
return this.invalidateByHref(href);
} else {
return [true];
}
})
).subscribe(() => {
invalidated$.next(true);
invalidated$.complete();
});
return combineLatest([response$, invalidated$]).pipe(
filter(([_, invalidated]) => invalidated),
map(([response, _]) => response),
);
} }
} }

View File

@@ -54,7 +54,8 @@ describe('BitstreamFormatDataService', () => {
function initTestService(halService) { function initTestService(halService) {
rd = createSuccessfulRemoteDataObject({}); rd = createSuccessfulRemoteDataObject({});
rdbService = jasmine.createSpyObj('rdbService', { rdbService = jasmine.createSpyObj('rdbService', {
buildFromRequestUUID: observableOf(rd) buildFromRequestUUID: observableOf(rd),
buildFromRequestUUIDAndAwait: observableOf(rd),
}); });
return new BitstreamFormatDataService( return new BitstreamFormatDataService(

View File

@@ -11,4 +11,5 @@ export class FindListOptions {
sort?: SortOptions; sort?: SortOptions;
searchParams?: RequestParam[]; searchParams?: RequestParam[];
startsWith?: string; startsWith?: string;
fetchThumbnail?: boolean;
} }

View File

@@ -202,6 +202,7 @@ describe('RelationshipDataService', () => {
}); });
it('should call getItemRelationshipsByLabel with the correct params', (done) => { it('should call getItemRelationshipsByLabel with the correct params', (done) => {
mockOptions = Object.assign(mockOptions, { fetchThumbnail: true });
service.getRelatedItemsByLabel( service.getRelatedItemsByLabel(
mockItem, mockItem,
mockLabel, mockLabel,
@@ -213,8 +214,8 @@ describe('RelationshipDataService', () => {
mockOptions, mockOptions,
true, true,
true, true,
followLink('leftItem'), followLink('leftItem',{}, followLink('thumbnail')),
followLink('rightItem'), followLink('rightItem',{}, followLink('thumbnail')),
followLink('relationshipType') followLink('relationshipType')
); );
done(); done();

View File

@@ -45,6 +45,7 @@ import { SearchData, SearchDataImpl } from './base/search-data';
import { PutData, PutDataImpl } from './base/put-data'; import { PutData, PutDataImpl } from './base/put-data';
import { IdentifiableDataService } from './base/identifiable-data.service'; import { IdentifiableDataService } from './base/identifiable-data.service';
import { dataService } from './base/data-service.decorator'; import { dataService } from './base/data-service.decorator';
import { itemLinksToFollow } from '../../shared/utils/relation-query.utils';
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
@@ -185,7 +186,7 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
]).pipe( ]).pipe(
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC), filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
take(1), take(1),
).subscribe(() => this.itemService.findByHref(item._links.self.href, false)); ).subscribe(() => this.itemService.findByHref(item._links.self.href));
} }
/** /**
@@ -258,7 +259,10 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
* @param options * @param options
*/ */
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> { getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(this.paginatedRelationsToItems(item.uuid)); let linksToFollow: FollowLinkConfig<Relationship>[] = itemLinksToFollow(options.fetchThumbnail);
linksToFollow.push(followLink('relationshipType'));
return this.getItemRelationshipsByLabel(item, label, options, true, true, ...linksToFollow).pipe(this.paginatedRelationsToItems(item.uuid));
} }
/** /**
@@ -516,14 +520,14 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
{ {
fieldName: 'relatedItem', fieldName: 'relatedItem',
fieldValue: itemId, fieldValue: itemId,
}, }
); );
}); });
return this.searchBy( return this.searchBy(
'byItemsAndType', 'byItemsAndType',
{ {
searchParams: searchParams, searchParams: searchParams
}, },
) as Observable<RemoteData<PaginatedList<Relationship>>>; ) as Observable<RemoteData<PaginatedList<Relationship>>>;

View File

@@ -307,7 +307,7 @@ describe('EPersonDataService', () => {
it('should sent a patch request with an uuid, token and new password to the epersons endpoint', () => { it('should sent a patch request with an uuid, token and new password to the epersons endpoint', () => {
service.patchPasswordWithToken('test-uuid', 'test-token', 'test-password'); service.patchPasswordWithToken('test-uuid', 'test-token', 'test-password');
const operation = Object.assign({ op: 'add', path: '/password', value: 'test-password' }); const operation = Object.assign({ op: 'add', path: '/password', value: { new_password: 'test-password' } });
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]); const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]);
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);

View File

@@ -3,7 +3,10 @@ import { createSelector, select, Store } from '@ngrx/store';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { find, map, take } from 'rxjs/operators'; import { find, map, take } from 'rxjs/operators';
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from '../../access-control/epeople-registry/epeople-registry.actions'; import {
EPeopleRegistryCancelEPersonAction,
EPeopleRegistryEditEPersonAction
} from '../../access-control/epeople-registry/epeople-registry.actions';
import { EPeopleRegistryState } from '../../access-control/epeople-registry/epeople-registry.reducers'; import { EPeopleRegistryState } from '../../access-control/epeople-registry/epeople-registry.reducers';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { hasNoValue, hasValue } from '../../shared/empty.util'; import { hasNoValue, hasValue } from '../../shared/empty.util';
@@ -318,7 +321,7 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
patchPasswordWithToken(uuid: string, token: string, password: string): Observable<RemoteData<EPerson>> { patchPasswordWithToken(uuid: string, token: string, password: string): Observable<RemoteData<EPerson>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const operation = Object.assign({ op: 'add', path: '/password', value: password }); const operation = Object.assign({ op: 'add', path: '/password', value: { 'new_password': password } });
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
map((endpoint: string) => this.getIDHref(endpoint, uuid)), map((endpoint: string) => this.getIDHref(endpoint, uuid)),

View File

@@ -26,6 +26,10 @@ import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
import { createPaginatedList, createRequestEntry$ } from '../../shared/testing/utils.test'; import { createPaginatedList, createRequestEntry$ } from '../../shared/testing/utils.test';
import { CoreState } from '../core-state.model'; import { CoreState } from '../core-state.model';
import { FindListOptions } from '../data/find-list-options.model'; import { FindListOptions } from '../data/find-list-options.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { getMockLinkService } from '../../shared/mocks/link-service.mock';
import { of as observableOf } from 'rxjs';
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
describe('GroupDataService', () => { describe('GroupDataService', () => {
let service: GroupDataService; let service: GroupDataService;
@@ -38,7 +42,7 @@ describe('GroupDataService', () => {
let groups$; let groups$;
let halService; let halService;
let rdbService; let rdbService;
let objectCache: ObjectCacheService;
function init() { function init() {
restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson';
groupsEndpoint = `${restEndpointURL}/groups`; groupsEndpoint = `${restEndpointURL}/groups`;
@@ -46,6 +50,7 @@ describe('GroupDataService', () => {
groups$ = createSuccessfulRemoteDataObject$(createPaginatedList(groups)); groups$ = createSuccessfulRemoteDataObject$(createPaginatedList(groups));
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ }); rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ });
halService = new HALEndpointServiceStub(restEndpointURL); halService = new HALEndpointServiceStub(restEndpointURL);
objectCache = new ObjectCacheService(store, getMockLinkService());
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
@@ -67,7 +72,7 @@ describe('GroupDataService', () => {
return new GroupDataService( return new GroupDataService(
requestService, requestService,
rdbService, rdbService,
null, objectCache,
halService, halService,
new DummyChangeAnalyzer() as any, new DummyChangeAnalyzer() as any,
null, null,
@@ -82,6 +87,7 @@ describe('GroupDataService', () => {
store = new Store<CoreState>(undefined, undefined, undefined); store = new Store<CoreState>(undefined, undefined, undefined);
service = initTestService(); service = initTestService();
spyOn(store, 'dispatch'); spyOn(store, 'dispatch');
spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();
}); });
describe('searchGroups', () => { describe('searchGroups', () => {
@@ -108,6 +114,10 @@ describe('GroupDataService', () => {
describe('addSubGroupToGroup', () => { describe('addSubGroupToGroup', () => {
beforeEach(() => { beforeEach(() => {
spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2']
} as ObjectCacheEntry));
spyOn((service as any).deleteData, 'invalidateByHref');
service.addSubGroupToGroup(GroupMock, GroupMock2).subscribe(); service.addSubGroupToGroup(GroupMock, GroupMock2).subscribe();
}); });
it('should send PostRequest to eperson/groups/group-id/subgroups endpoint with new subgroup link in body', () => { it('should send PostRequest to eperson/groups/group-id/subgroups endpoint with new subgroup link in body', () => {
@@ -118,20 +128,50 @@ describe('GroupDataService', () => {
const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options); const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options);
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should invalidate the previous requests of the parent group', () => {
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
callback();
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
});
}); });
describe('deleteSubGroupFromGroup', () => { describe('deleteSubGroupFromGroup', () => {
beforeEach(() => { beforeEach(() => {
spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2']
} as ObjectCacheEntry));
spyOn((service as any).deleteData, 'invalidateByHref');
service.deleteSubGroupFromGroup(GroupMock, GroupMock2).subscribe(); service.deleteSubGroupFromGroup(GroupMock, GroupMock2).subscribe();
}); });
it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => { it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => {
const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id); const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id);
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should invalidate the previous requests of the parent group\'', () => {
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
callback();
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
});
}); });
describe('addMemberToGroup', () => { describe('addMemberToGroup', () => {
beforeEach(() => { beforeEach(() => {
spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2']
} as ObjectCacheEntry));
spyOn((service as any).deleteData, 'invalidateByHref');
service.addMemberToGroup(GroupMock, EPersonMock2).subscribe(); service.addMemberToGroup(GroupMock, EPersonMock2).subscribe();
}); });
it('should send PostRequest to eperson/groups/group-id/epersons endpoint with new eperson member in body', () => { it('should send PostRequest to eperson/groups/group-id/epersons endpoint with new eperson member in body', () => {
@@ -142,20 +182,48 @@ describe('GroupDataService', () => {
const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options); const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options);
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should invalidate the previous requests of the EPerson and the group', () => {
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
callback();
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock2._links.self.href);
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
});
}); });
describe('deleteMemberFromGroup', () => { describe('deleteMemberFromGroup', () => {
beforeEach(() => { beforeEach(() => {
spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2']
} as ObjectCacheEntry));
spyOn((service as any).deleteData, 'invalidateByHref');
service.deleteMemberFromGroup(GroupMock, EPersonMock).subscribe(); service.deleteMemberFromGroup(GroupMock, EPersonMock).subscribe();
}); });
it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => { it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => {
const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id); const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id);
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should invalidate the previous requests of the EPerson and the group', () => {
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
callback();
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock._links.self.href);
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
});
}); });
describe('editGroup', () => { describe('editGroup', () => {
it('should dispatch a EDIT_GROUP action with the groupp to start editing', () => { it('should dispatch a EDIT_GROUP action with the group to start editing', () => {
service.editGroup(GroupMock); service.editGroup(GroupMock);
expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryEditGroupAction(GroupMock)); expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryEditGroupAction(GroupMock));
}); });

View File

@@ -2,7 +2,7 @@ import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { createSelector, select, Store } from '@ngrx/store'; import { createSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable, zip as observableZip } from 'rxjs';
import { filter, map, take } from 'rxjs/operators'; import { filter, map, take } from 'rxjs/operators';
import { import {
GroupRegistryCancelGroupAction, GroupRegistryCancelGroupAction,
@@ -124,7 +124,8 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
} }
/** /**
* Adds given subgroup as a subgroup to the given active group * Adds given subgroup as a subgroup to the given active group and waits until the {@link activeGroup} and
* the {@link subgroup} are invalidated.
* @param activeGroup Group we want to add subgroup to * @param activeGroup Group we want to add subgroup to
* @param subgroup Group we want to add as subgroup to activeGroup * @param subgroup Group we want to add as subgroup to activeGroup
*/ */
@@ -137,11 +138,16 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options); const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options);
this.requestService.send(postRequest); this.requestService.send(postRequest);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
this.invalidateByHref(activeGroup._links.self.href),
this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)),
));
} }
/** /**
* Deletes a given subgroup from the subgroups of the given active group * Deletes a given subgroup from the subgroups of the given active group and waits until the {@link activeGroup} and
* the {@link subgroup} are invalidated.
* are invalidated.
* @param activeGroup Group we want to delete subgroup from * @param activeGroup Group we want to delete subgroup from
* @param subgroup Subgroup we want to delete from activeGroup * @param subgroup Subgroup we want to delete from activeGroup
*/ */
@@ -150,11 +156,15 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id); const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id);
this.requestService.send(deleteRequest); this.requestService.send(deleteRequest);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
this.invalidateByHref(activeGroup._links.self.href),
this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)),
));
} }
/** /**
* Adds given ePerson as member to given group * Adds given ePerson as member to a given group and invalidates the ePerson and waits until the {@link ePerson} and
* the {@link activeGroup} are invalidated.
* @param activeGroup Group we want to add member to * @param activeGroup Group we want to add member to
* @param ePerson EPerson we want to add as member to given activeGroup * @param ePerson EPerson we want to add as member to given activeGroup
*/ */
@@ -167,11 +177,17 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options); const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options);
this.requestService.send(postRequest); this.requestService.send(postRequest);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
this.invalidateByHref(ePerson._links.self.href),
this.invalidateByHref(activeGroup._links.self.href),
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
));
} }
/** /**
* Deletes a given ePerson from the members of the given active group * Deletes a given ePerson from the members of the given active group and waits until the {@link ePerson} and the
* {@link activeGroup} are invalidated.
* @param activeGroup Group we want to delete member from * @param activeGroup Group we want to delete member from
* @param ePerson EPerson we want to delete from members of given activeGroup * @param ePerson EPerson we want to delete from members of given activeGroup
*/ */
@@ -180,7 +196,12 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id); const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id);
this.requestService.send(deleteRequest); this.requestService.send(deleteRequest);
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
this.invalidateByHref(ePerson._links.self.href),
this.invalidateByHref(activeGroup._links.self.href),
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
));
} }
/** /**
@@ -276,7 +297,7 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
* @param role The name of the role for which to create a group * @param role The name of the role for which to create a group
* @param link The REST endpoint to create the group * @param link The REST endpoint to create the group
*/ */
createComcolGroup(dso: Community|Collection, role: string, link: string): Observable<RemoteData<Group>> { createComcolGroup(dso: Community | Collection, role: string, link: string): Observable<RemoteData<Group>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const group = Object.assign(new Group(), { const group = Object.assign(new Group(), {

View File

@@ -19,6 +19,7 @@ import { RequestEntry } from '../data/request-entry.model';
import { FindListOptions } from '../data/find-list-options.model'; import { FindListOptions } from '../data/find-list-options.model';
import { EPersonDataService } from '../eperson/eperson-data.service'; import { EPersonDataService } from '../eperson/eperson-data.service';
import { GroupDataService } from '../eperson/group-data.service'; import { GroupDataService } from '../eperson/group-data.service';
import { RestRequestMethod } from '../data/rest-request-method';
describe('ResourcePolicyService', () => { describe('ResourcePolicyService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -120,12 +121,23 @@ describe('ResourcePolicyService', () => {
}), }),
buildFromRequestUUID: hot('a|', { buildFromRequestUUID: hot('a|', {
a: resourcePolicyRD a: resourcePolicyRD
}),
buildFromRequestUUIDAndAwait: hot('a|', {
a: resourcePolicyRD
}) })
}); });
ePersonService = jasmine.createSpyObj('ePersonService', { ePersonService = jasmine.createSpyObj('ePersonService', {
getBrowseEndpoint: hot('a', { getBrowseEndpoint: hot('a', {
a: ePersonEndpoint a: ePersonEndpoint
}), }),
getIDHrefObs: cold('a', {
a: 'https://rest.api/rest/api/eperson/epersons/' + epersonUUID
}),
});
groupService = jasmine.createSpyObj('groupService', {
getIDHrefObs: cold('a', {
a: 'https://rest.api/rest/api/eperson/groups/' + groupUUID
}),
}); });
objectCache = {} as ObjectCacheService; objectCache = {} as ObjectCacheService;
const notificationsService = {} as NotificationsService; const notificationsService = {} as NotificationsService;
@@ -142,11 +154,12 @@ describe('ResourcePolicyService', () => {
groupService, groupService,
); );
spyOn(service, 'findById').and.callThrough();
spyOn(service, 'findByHref').and.callThrough();
spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
spyOn((service as any).createData, 'create').and.callThrough(); spyOn((service as any).createData, 'create').and.callThrough();
spyOn((service as any).deleteData, 'delete').and.callThrough(); spyOn((service as any).deleteData, 'delete').and.callThrough();
spyOn((service as any).patchData, 'update').and.callThrough(); spyOn((service as any).patchData, 'update').and.callThrough();
spyOn((service as any), 'findById').and.callThrough();
spyOn((service as any), 'findByHref').and.callThrough();
spyOn((service as any).searchData, 'searchBy').and.callThrough(); spyOn((service as any).searchData, 'searchBy').and.callThrough();
spyOn((service as any).searchData, 'getSearchByHref').and.returnValue(observableOf(requestURL)); spyOn((service as any).searchData, 'getSearchByHref').and.returnValue(observableOf(requestURL));
}); });
@@ -318,14 +331,32 @@ describe('ResourcePolicyService', () => {
}); });
describe('updateTarget', () => { describe('updateTarget', () => {
it('should create a new PUT request for eperson', () => { beforeEach(() => {
const targetType = 'eperson'; scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, epersonUUID));
});
const result = service.updateTarget(resourcePolicyId, requestURL, epersonUUID, targetType); it('should send a PUT request to update the EPerson', () => {
const expected = cold('a|', { service.updateTarget(resourcePolicyId, requestURL, epersonUUID, 'eperson');
a: resourcePolicyRD scheduler.flush();
});
expect(result).toBeObservable(expected); expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
method: RestRequestMethod.PUT,
uuid: requestUUID,
href: `${resourcePolicy._links.self.href}/eperson`,
body: 'https://rest.api/rest/api/eperson/epersons/' + epersonUUID,
}));
});
it('should invalidate the ResourcePolicy', () => {
service.updateTarget(resourcePolicyId, requestURL, epersonUUID, 'eperson');
scheduler.flush();
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect((rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
callback();
expect(service.invalidateByHref).toHaveBeenCalledWith(resourcePolicy._links.self.href);
}); });
}); });

View File

@@ -16,7 +16,7 @@ import { PaginatedList } from '../data/paginated-list.model';
import { ActionType } from './models/action-type.model'; import { ActionType } from './models/action-type.model';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { map, take } from 'rxjs/operators'; import { first, map } from 'rxjs/operators';
import { NoContent } from '../shared/NoContent.model'; import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators'; import { getFirstCompletedRemoteData } from '../shared/operators';
import { FindListOptions } from '../data/find-list-options.model'; import { FindListOptions } from '../data/find-list-options.model';
@@ -194,13 +194,8 @@ export class ResourcePolicyDataService extends IdentifiableDataService<ResourceP
* @param targetType the type of the target (eperson or group) to which the permission is being granted * @param targetType the type of the target (eperson or group) to which the permission is being granted
*/ */
updateTarget(resourcePolicyId: string, resourcePolicyHref: string, targetUUID: string, targetType: string): Observable<RemoteData<any>> { updateTarget(resourcePolicyId: string, resourcePolicyHref: string, targetUUID: string, targetType: string): Observable<RemoteData<any>> {
const targetService = targetType === 'eperson' ? this.ePersonService : this.groupService; const targetService = targetType === 'eperson' ? this.ePersonService : this.groupService;
const targetEndpoint$ = targetService.getIDHrefObs(targetUUID);
const targetEndpoint$ = targetService.getBrowseEndpoint().pipe(
take(1),
map((endpoint: string) =>`${endpoint}/${targetUUID}`),
);
const options: HttpOptions = Object.create({}); const options: HttpOptions = Object.create({});
let headers = new HttpHeaders(); let headers = new HttpHeaders();
@@ -209,9 +204,9 @@ export class ResourcePolicyDataService extends IdentifiableDataService<ResourceP
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
this.requestService.setStaleByHrefSubstring(`${this.getLinkPath()}/${resourcePolicyId}/${targetType}`); targetEndpoint$.pipe(
first(),
targetEndpoint$.subscribe((targetEndpoint) => { ).subscribe((targetEndpoint) => {
const resourceEndpoint = resourcePolicyHref + '/' + targetType; const resourceEndpoint = resourcePolicyHref + '/' + targetType;
const request = new PutRequest(requestId, resourceEndpoint, targetEndpoint, options); const request = new PutRequest(requestId, resourceEndpoint, targetEndpoint, options);
Object.assign(request, { Object.assign(request, {
@@ -222,8 +217,7 @@ export class ResourcePolicyDataService extends IdentifiableDataService<ResourceP
this.requestService.send(request); this.requestService.send(request);
}); });
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(resourcePolicyHref));
} }
} }

View File

@@ -40,6 +40,12 @@ export class BrowseEntry extends ListableObject implements TypedObject {
@autoserializeAs('valueLang') @autoserializeAs('valueLang')
language: string; language: string;
/**
* Thumbnail link used when browsing items with showThumbs config enabled.
*/
@autoserializeAs('thumbnail')
thumbnail: string;
/** /**
* The count of this browse entry * The count of this browse entry
*/ */
@@ -51,6 +57,7 @@ export class BrowseEntry extends ListableObject implements TypedObject {
_links: { _links: {
self: HALLink; self: HALLink;
entries: HALLink; entries: HALLink;
thumbnail: HALLink;
}; };
/** /**

View File

@@ -25,6 +25,7 @@ import { PaginationService } from '../../pagination/pagination.service';
import { SearchConfigurationService } from './search-configuration.service'; import { SearchConfigurationService } from './search-configuration.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { RequestEntry } from '../../data/request-entry.model'; import { RequestEntry } from '../../data/request-entry.model';
import { Angulartics2 } from 'angulartics2';
@Component({ template: '' }) @Component({ template: '' })
class DummyComponent { class DummyComponent {
@@ -57,6 +58,7 @@ describe('SearchService', () => {
{ provide: DSpaceObjectDataService, useValue: {} }, { provide: DSpaceObjectDataService, useValue: {} },
{ provide: PaginationService, useValue: {} }, { provide: PaginationService, useValue: {} },
{ provide: SearchConfigurationService, useValue: searchConfigService }, { provide: SearchConfigurationService, useValue: searchConfigService },
{ provide: Angulartics2, useValue: {} },
SearchService SearchService
], ],
}); });
@@ -124,6 +126,7 @@ describe('SearchService', () => {
{ provide: DSpaceObjectDataService, useValue: {} }, { provide: DSpaceObjectDataService, useValue: {} },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: SearchConfigurationService, useValue: searchConfigService }, { provide: SearchConfigurationService, useValue: searchConfigService },
{ provide: Angulartics2, useValue: {} },
SearchService SearchService
], ],
}); });

View File

@@ -33,6 +33,7 @@ import { SearchConfigurationService } from './search-configuration.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { RestRequest } from '../../data/rest-request.model'; import { RestRequest } from '../../data/rest-request.model';
import { BaseDataService } from '../../data/base/base-data.service'; import { BaseDataService } from '../../data/base/base-data.service';
import { Angulartics2 } from 'angulartics2';
/** /**
* A limited data service implementation for the 'discover' endpoint * A limited data service implementation for the 'discover' endpoint
@@ -96,6 +97,7 @@ export class SearchService implements OnDestroy {
private dspaceObjectService: DSpaceObjectDataService, private dspaceObjectService: DSpaceObjectDataService,
private paginationService: PaginationService, private paginationService: PaginationService,
private searchConfigurationService: SearchConfigurationService, private searchConfigurationService: SearchConfigurationService,
private angulartics2: Angulartics2,
) { ) {
this.searchDataService = new SearchDataService(); this.searchDataService = new SearchDataService();
} }
@@ -320,6 +322,37 @@ export class SearchService implements OnDestroy {
}); });
} }
/**
* Send search event to rest api using angularitics
* @param config Paginated search options used
* @param searchQueryResponse The response objects of the performed search
*/
trackSearch(config: PaginatedSearchOptions, searchQueryResponse: SearchObjects<DSpaceObject>) {
const filters: { filter: string, operator: string, value: string, label: string; }[] = [];
const appliedFilters = searchQueryResponse.appliedFilters || [];
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
const appliedFilter = appliedFilters[i];
filters.push(appliedFilter);
}
this.angulartics2.eventTrack.next({
action: 'search',
properties: {
searchOptions: config,
page: {
size: config.pagination.size, // same as searchQueryResponse.page.elementsPerPage
totalElements: searchQueryResponse.pageInfo.totalElements,
totalPages: searchQueryResponse.pageInfo.totalPages,
number: config.pagination.currentPage, // same as searchQueryResponse.page.currentPage
},
sort: {
by: config.sort.field,
order: config.sort.direction
},
filters: filters,
},
});
}
/** /**
* @returns {string} The base path to the search page * @returns {string} The base path to the search page
*/ */

View File

@@ -0,0 +1,17 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import { SubmissionCcLicenseDataService } from './submission-cc-license-data.service';
import { testFindAllDataImplementation } from '../data/base/find-all-data.spec';
describe('SubmissionCcLicenseDataService', () => {
describe('composition', () => {
const initService = () => new SubmissionCcLicenseDataService(null, null, null, null);
testFindAllDataImplementation(initService);
});
});

View File

@@ -6,7 +6,7 @@ import { RequestService } from '../data/request.service';
import { SUBMISSION_CC_LICENSE } from './models/submission-cc-licence.resource-type'; import { SUBMISSION_CC_LICENSE } from './models/submission-cc-licence.resource-type';
import { SubmissionCcLicence } from './models/submission-cc-license.model'; import { SubmissionCcLicence } from './models/submission-cc-license.model';
import { BaseDataService } from '../data/base/base-data.service'; import { BaseDataService } from '../data/base/base-data.service';
import { FindAllData } from '../data/base/find-all-data'; import {FindAllData, FindAllDataImpl} from '../data/base/find-all-data';
import { FindListOptions } from '../data/find-list-options.model'; import { FindListOptions } from '../data/find-list-options.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -19,6 +19,7 @@ import { dataService } from '../data/base/data-service.decorator';
export class SubmissionCcLicenseDataService extends BaseDataService<SubmissionCcLicence> implements FindAllData<SubmissionCcLicence> { export class SubmissionCcLicenseDataService extends BaseDataService<SubmissionCcLicence> implements FindAllData<SubmissionCcLicence> {
protected linkPath = 'submissioncclicenses'; protected linkPath = 'submissioncclicenses';
private findAllData: FindAllData<SubmissionCcLicence>;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
@@ -27,6 +28,8 @@ export class SubmissionCcLicenseDataService extends BaseDataService<SubmissionCc
protected halService: HALEndpointService, protected halService: HALEndpointService,
) { ) {
super('submissioncclicenses', requestService, rdbService, objectCache, halService); super('submissioncclicenses', requestService, rdbService, objectCache, halService);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
} }
/** /**
@@ -44,6 +47,6 @@ export class SubmissionCcLicenseDataService extends BaseDataService<SubmissionCc
* Return an observable that emits object list * Return an observable that emits object list
*/ */
public findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SubmissionCcLicence>[]): Observable<RemoteData<PaginatedList<SubmissionCcLicence>>> { public findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SubmissionCcLicence>[]): Observable<RemoteData<PaginatedList<SubmissionCcLicence>>> {
return undefined; return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
} }
} }

View File

@@ -21,7 +21,7 @@
<div class="card-body"> <div class="card-body">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4> <h4 class="card-title" [innerHTML]="dsoTitle"></h4>
</ds-truncatable-part> </ds-truncatable-part>
<p *ngIf="dso.hasMetadata('creativework.datePublished')" <p *ngIf="dso.hasMetadata('creativework.datePublished')"
class="item-date card-text text-muted"> class="item-date card-text text-muted">

View File

@@ -21,7 +21,7 @@
<div class="card-body"> <div class="card-body">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4> <h4 class="card-title" [innerHTML]="dsoTitle"></h4>
</ds-truncatable-part> </ds-truncatable-part>
<p *ngIf="dso.hasMetadata('creativework.datePublished')" <p *ngIf="dso.hasMetadata('creativework.datePublished')"
class="item-date card-text text-muted"> class="item-date card-text text-muted">

View File

@@ -21,7 +21,7 @@
<div class="card-body"> <div class="card-body">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4> <h4 class="card-title" [innerHTML]="dsoTitle"></h4>
</ds-truncatable-part> </ds-truncatable-part>
<p *ngIf="dso.hasMetadata('creativework.editor')" <p *ngIf="dso.hasMetadata('creativework.editor')"
class="item-publisher card-text text-muted"> class="item-publisher card-text text-muted">

View File

@@ -1,12 +1,23 @@
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <div class="row">
<ds-truncatable [id]="dso.id"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" rel="noopener noreferrer"
[innerHTML]="dsoTitle"></a> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out">
<span *ngIf="linkType == linkTypes.None" <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
class="lead item-list-title dont-break-out" </ds-thumbnail>
[innerHTML]="dsoTitle"></span> </a>
<span class="text-muted"> </div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span>
<span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1"> <ds-truncatable-part [id]="dso.id" [minLines]="1">
<span *ngIf="dso.allMetadata(['publicationvolume.volumeNumber']).length > 0" <span *ngIf="dso.allMetadata(['publicationvolume.volumeNumber']).length > 0"
class="item-list-journal-issues"> class="item-list-journal-issues">
@@ -22,4 +33,6 @@
</span> </span>
</ds-truncatable-part> </ds-truncatable-part>
</span> </span>
</ds-truncatable> </ds-truncatable>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
let journalIssueListElementComponent: JournalIssueSearchResultListElementComponent; let journalIssueListElementComponent: JournalIssueSearchResultListElementComponent;
let fixture: ComponentFixture<JournalIssueSearchResultListElementComponent>; let fixture: ComponentFixture<JournalIssueSearchResultListElementComponent>;
@@ -57,13 +58,26 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
}) })
}); });
const environmentUseThumbs = {
browseBy: {
showThumbnails: true
}
};
const enviromentNoThumbs = {
browseBy: {
showThumbnails: false
}
};
describe('JournalIssueSearchResultListElementComponent', () => { describe('JournalIssueSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [JournalIssueSearchResultListElementComponent, TruncatePipe], declarations: [JournalIssueSearchResultListElementComponent, TruncatePipe],
providers: [ providers: [
{ provide: TruncatableService, useValue: {} }, { provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock } { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -78,6 +92,22 @@ describe('JournalIssueSearchResultListElementComponent', () => {
})); }));
describe('with environment.browseBy.showThumbnails set to true', () => {
beforeEach(() => {
journalIssueListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should set showThumbnails to true', () => {
expect(journalIssueListElementComponent.showThumbnails).toBeTrue();
});
it('should add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeTruthy();
});
});
describe('When the item has a journal identifier', () => { describe('When the item has a journal identifier', () => {
beforeEach(() => { beforeEach(() => {
journalIssueListElementComponent.object = mockItemWithMetadata; journalIssueListElementComponent.object = mockItemWithMetadata;
@@ -126,3 +156,39 @@ describe('JournalIssueSearchResultListElementComponent', () => {
}); });
}); });
}); });
describe('JournalIssueSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [JournalIssueSearchResultListElementComponent, TruncatePipe],
providers: [
{provide: TruncatableService, useValue: {}},
{provide: DSONameService, useClass: DSONameServiceMock},
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(JournalIssueSearchResultListElementComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default}
}).compileComponents();
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(JournalIssueSearchResultListElementComponent);
journalIssueListElementComponent = fixture.componentInstance;
}));
describe('with environment.browseBy.showThumbnails set to false', () => {
beforeEach(() => {
journalIssueListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should not add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeFalsy();
});
});
});

View File

@@ -13,4 +13,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
* The component for displaying a list element for an item search result of the type Journal Issue * The component for displaying a list element for an item search result of the type Journal Issue
*/ */
export class JournalIssueSearchResultListElementComponent extends ItemSearchResultListElementComponent { export class JournalIssueSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnails if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
} }

View File

@@ -1,12 +1,23 @@
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <div class="row">
<ds-truncatable [id]="dso.id"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" rel="noopener noreferrer"
[innerHTML]="dsoTitle"></a> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out">
<span *ngIf="linkType == linkTypes.None" <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
class="lead item-list-title dont-break-out" </ds-thumbnail>
[innerHTML]="dsoTitle"></span> </a>
<span class="text-muted"> </div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span>
<span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1"> <ds-truncatable-part [id]="dso.id" [minLines]="1">
<span *ngIf="dso.allMetadata(['journal.title']).length > 0" <span *ngIf="dso.allMetadata(['journal.title']).length > 0"
class="item-list-journal-volumes"> class="item-list-journal-volumes">
@@ -22,4 +33,6 @@
</span> </span>
</ds-truncatable-part> </ds-truncatable-part>
</span> </span>
</ds-truncatable> </ds-truncatable>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
let journalVolumeListElementComponent: JournalVolumeSearchResultListElementComponent; let journalVolumeListElementComponent: JournalVolumeSearchResultListElementComponent;
let fixture: ComponentFixture<JournalVolumeSearchResultListElementComponent>; let fixture: ComponentFixture<JournalVolumeSearchResultListElementComponent>;
@@ -56,6 +57,18 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
}) })
}); });
const environmentUseThumbs = {
browseBy: {
showThumbnails: true
}
};
const enviromentNoThumbs = {
browseBy: {
showThumbnails: false
}
};
describe('JournalVolumeSearchResultListElementComponent', () => { describe('JournalVolumeSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -63,6 +76,7 @@ describe('JournalVolumeSearchResultListElementComponent', () => {
providers: [ providers: [
{ provide: TruncatableService, useValue: {} }, { provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }, { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -77,6 +91,21 @@ describe('JournalVolumeSearchResultListElementComponent', () => {
})); }));
describe('with environment.browseBy.showThumbnails set to true', () => {
beforeEach(() => {
journalVolumeListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should set showThumbnails to true', () => {
expect(journalVolumeListElementComponent.showThumbnails).toBeTrue();
});
it('should add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeTruthy();
});
});
describe('When the item has a journal title', () => { describe('When the item has a journal title', () => {
beforeEach(() => { beforeEach(() => {
journalVolumeListElementComponent.object = mockItemWithMetadata; journalVolumeListElementComponent.object = mockItemWithMetadata;
@@ -125,3 +154,38 @@ describe('JournalVolumeSearchResultListElementComponent', () => {
}); });
}); });
}); });
describe('JournalVolumeSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [JournalVolumeSearchResultListElementComponent, TruncatePipe],
providers: [
{provide: TruncatableService, useValue: {}},
{provide: DSONameService, useClass: DSONameServiceMock},
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(JournalVolumeSearchResultListElementComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default}
}).compileComponents();
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(JournalVolumeSearchResultListElementComponent);
journalVolumeListElementComponent = fixture.componentInstance;
}));
describe('with environment.browseBy.showThumbnails set to false', () => {
beforeEach(() => {
journalVolumeListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should not add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeFalsy();
});
});
});

View File

@@ -13,4 +13,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
* The component for displaying a list element for an item search result of the type Journal Volume * The component for displaying a list element for an item search result of the type Journal Volume
*/ */
export class JournalVolumeSearchResultListElementComponent extends ItemSearchResultListElementComponent { export class JournalVolumeSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnails if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
} }

View File

@@ -1,12 +1,21 @@
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <div class="row">
<ds-truncatable [id]="dso.id"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out">
[innerHTML]="dsoTitle"></a> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
<span *ngIf="linkType == linkTypes.None" </ds-thumbnail>
class="lead item-list-title dont-break-out" </a>
[innerHTML]="dsoTitle"></span> </div>
<span class="text-muted"> <div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span>
<span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1"> <ds-truncatable-part [id]="dso.id" [minLines]="1">
<span *ngIf="dso.allMetadata(['creativeworkseries.issn']).length > 0" <span *ngIf="dso.allMetadata(['creativeworkseries.issn']).length > 0"
class="item-list-journals"> class="item-list-journals">
@@ -16,4 +25,6 @@
</span> </span>
</ds-truncatable-part> </ds-truncatable-part>
</span> </span>
</ds-truncatable> </ds-truncatable>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
let journalListElementComponent: JournalSearchResultListElementComponent; let journalListElementComponent: JournalSearchResultListElementComponent;
let fixture: ComponentFixture<JournalSearchResultListElementComponent>; let fixture: ComponentFixture<JournalSearchResultListElementComponent>;
@@ -52,6 +53,18 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
} }
); );
const environmentUseThumbs = {
browseBy: {
showThumbnails: true
}
};
const enviromentNoThumbs = {
browseBy: {
showThumbnails: false
}
};
describe('JournalSearchResultListElementComponent', () => { describe('JournalSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -59,6 +72,7 @@ describe('JournalSearchResultListElementComponent', () => {
providers: [ providers: [
{ provide: TruncatableService, useValue: {} }, { provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }, { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -73,6 +87,21 @@ describe('JournalSearchResultListElementComponent', () => {
})); }));
describe('with environment.browseBy.showThumbnails set to true', () => {
beforeEach(() => {
journalListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should set showThumbnails to true', () => {
expect(journalListElementComponent.showThumbnails).toBeTrue();
});
it('should add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeTruthy();
});
});
describe('When the item has an issn', () => { describe('When the item has an issn', () => {
beforeEach(() => { beforeEach(() => {
journalListElementComponent.object = mockItemWithMetadata; journalListElementComponent.object = mockItemWithMetadata;
@@ -97,3 +126,39 @@ describe('JournalSearchResultListElementComponent', () => {
}); });
}); });
}); });
describe('JournalSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [JournalSearchResultListElementComponent, TruncatePipe],
providers: [
{provide: TruncatableService, useValue: {}},
{provide: DSONameService, useClass: DSONameServiceMock},
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(JournalSearchResultListElementComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default}
}).compileComponents();
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(JournalSearchResultListElementComponent);
journalListElementComponent = fixture.componentInstance;
}));
describe('with environment.browseBy.showThumbnails set to false', () => {
beforeEach(() => {
journalListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should not add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeFalsy();
});
});
});

View File

@@ -13,4 +13,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
* The component for displaying a list element for an item search result of the type Journal * The component for displaying a list element for an item search result of the type Journal
*/ */
export class JournalSearchResultListElementComponent extends ItemSearchResultListElementComponent { export class JournalSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnails if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
} }

View File

@@ -1,7 +1,6 @@
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<h2 class="item-page-title-field mr-auto"> <ds-item-page-title-field [item]="object" class="mr-auto">
{{'journalissue.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values> </ds-item-page-title-field>
</h2>
<div class="pl-2 space-children-mr"> <div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object" <ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'" [tooltipMsgCreate]="'item.page.version.create'"

View File

@@ -1,7 +1,6 @@
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<h2 class="item-page-title-field mr-auto"> <ds-item-page-title-field [item]="object" class="mr-auto">
{{'journalvolume.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values> </ds-item-page-title-field>
</h2>
<div class="pl-2 space-children-mr"> <div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object" <ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'" [tooltipMsgCreate]="'item.page.version.create'"

View File

@@ -1,7 +1,6 @@
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<h2 class="item-page-title-field mr-auto"> <ds-item-page-title-field [item]="object" class="mr-auto">
{{'journal.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values> </ds-item-page-title-field>
</h2>
<div class="pl-2 space-children-mr"> <div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object" <ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'" [tooltipMsgCreate]="'item.page.version.create'"

View File

@@ -21,7 +21,7 @@
<div class="card-body"> <div class="card-body">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
<h4 class="card-title" [innerHTML]="firstMetadataValue('organization.legalName')"></h4> <h4 class="card-title" [innerHTML]="dsoTitle"></h4>
</ds-truncatable-part> </ds-truncatable-part>
<p *ngIf="dso.hasMetadata('organization.foundingDate')" <p *ngIf="dso.hasMetadata('organization.foundingDate')"
class="item-date card-text text-muted"> class="item-date card-text text-muted">

View File

@@ -21,8 +21,7 @@
<div class="card-body"> <div class="card-body">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
<h4 class="card-title" <h4 class="card-title" [innerHTML]="dsoTitle"></h4>
[innerHTML]="firstMetadataValue('person.familyName') + ', ' + firstMetadataValue('person.givenName')"></h4>
</ds-truncatable-part> </ds-truncatable-part>
<p *ngIf="dso.hasMetadata('person.email')" class="item-email card-text text-muted"> <p *ngIf="dso.hasMetadata('person.email')" class="item-email card-text text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1"> <ds-truncatable-part [id]="dso.id" [minLines]="1">

View File

@@ -21,7 +21,7 @@
<div class="card-body"> <div class="card-body">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4> <h4 class="card-title" [innerHTML]="dsoTitle"></h4>
</ds-truncatable-part> </ds-truncatable-part>
<p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text text-muted"> <p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="3"> <ds-truncatable-part [id]="dso.id" [minLines]="3">

View File

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

View File

@@ -9,6 +9,7 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
let orgUnitListElementComponent: OrgUnitSearchResultListElementComponent; let orgUnitListElementComponent: OrgUnitSearchResultListElementComponent;
let fixture: ComponentFixture<OrgUnitSearchResultListElementComponent>; let fixture: ComponentFixture<OrgUnitSearchResultListElementComponent>;
@@ -50,13 +51,26 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
}) })
}); });
const environmentUseThumbs = {
browseBy: {
showThumbnails: true
}
};
const enviromentNoThumbs = {
browseBy: {
showThumbnails: false
}
};
describe('OrgUnitSearchResultListElementComponent', () => { describe('OrgUnitSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ OrgUnitSearchResultListElementComponent , TruncatePipe], declarations: [ OrgUnitSearchResultListElementComponent , TruncatePipe],
providers: [ providers: [
{ provide: TruncatableService, useValue: {} }, { provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock } { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
], ],
schemas: [ NO_ERRORS_SCHEMA ] schemas: [ NO_ERRORS_SCHEMA ]
@@ -71,6 +85,21 @@ describe('OrgUnitSearchResultListElementComponent', () => {
})); }));
describe('with environment.browseBy.showThumbnails set to true', () => {
beforeEach(() => {
orgUnitListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should set showThumbnails to true', () => {
expect(orgUnitListElementComponent.showThumbnails).toBeTrue();
});
it('should add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeTruthy();
});
});
describe('When the item has an org unit description', () => { describe('When the item has an org unit description', () => {
beforeEach(() => { beforeEach(() => {
orgUnitListElementComponent.object = mockItemWithMetadata; orgUnitListElementComponent.object = mockItemWithMetadata;
@@ -95,3 +124,39 @@ describe('OrgUnitSearchResultListElementComponent', () => {
}); });
}); });
}); });
describe('OrgUnitSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OrgUnitSearchResultListElementComponent, TruncatePipe],
providers: [
{provide: TruncatableService, useValue: {}},
{provide: DSONameService, useClass: DSONameServiceMock},
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(OrgUnitSearchResultListElementComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default}
}).compileComponents();
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(OrgUnitSearchResultListElementComponent);
orgUnitListElementComponent = fixture.componentInstance;
}));
describe('with environment.browseBy.showThumbnails set to false', () => {
beforeEach(() => {
orgUnitListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should not add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeNull();
});
});
});

View File

@@ -13,4 +13,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
* The component for displaying a list element for an item search result of the type Organisation Unit * The component for displaying a list element for an item search result of the type Organisation Unit
*/ */
export class OrgUnitSearchResultListElementComponent extends ItemSearchResultListElementComponent { export class OrgUnitSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnail if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
} }

View File

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

View File

@@ -9,6 +9,7 @@ import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
let personListElementComponent: PersonSearchResultListElementComponent; let personListElementComponent: PersonSearchResultListElementComponent;
let fixture: ComponentFixture<PersonSearchResultListElementComponent>; let fixture: ComponentFixture<PersonSearchResultListElementComponent>;
@@ -50,13 +51,26 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
}) })
}); });
const environmentUseThumbs = {
browseBy: {
showThumbnails: true
}
};
const enviromentNoThumbs = {
browseBy: {
showThumbnails: false
}
};
describe('PersonSearchResultListElementComponent', () => { describe('PersonSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PersonSearchResultListElementComponent, TruncatePipe], declarations: [PersonSearchResultListElementComponent, TruncatePipe],
providers: [ providers: [
{ provide: TruncatableService, useValue: {} }, { provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock } { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -71,6 +85,21 @@ describe('PersonSearchResultListElementComponent', () => {
})); }));
describe('with environment.browseBy.showThumbnails set to true', () => {
beforeEach(() => {
personListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should set showThumbnails to true', () => {
expect(personListElementComponent.showThumbnails).toBeTrue();
});
it('should add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeTruthy();
});
});
describe('When the item has a job title', () => { describe('When the item has a job title', () => {
beforeEach(() => { beforeEach(() => {
personListElementComponent.object = mockItemWithMetadata; personListElementComponent.object = mockItemWithMetadata;
@@ -95,3 +124,39 @@ describe('PersonSearchResultListElementComponent', () => {
}); });
}); });
}); });
describe('PersonSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PersonSearchResultListElementComponent, TruncatePipe],
providers: [
{provide: TruncatableService, useValue: {}},
{provide: DSONameService, useClass: DSONameServiceMock},
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(PersonSearchResultListElementComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default}
}).compileComponents();
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(PersonSearchResultListElementComponent);
personListElementComponent = fixture.componentInstance;
}));
describe('with environment.browseBy.showThumbnails set to false', () => {
beforeEach(() => {
personListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should not add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeFalsy();
});
});
});

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { import {
listableObjectComponent listableObjectComponent
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
@@ -8,6 +8,7 @@ import {
} from '../../../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; } from '../../../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement) @listableObjectComponent('PersonSearchResult', ViewMode.ListElement)
@Component({ @Component({
@@ -20,14 +21,21 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service
*/ */
export class PersonSearchResultListElementComponent extends ItemSearchResultListElementComponent { export class PersonSearchResultListElementComponent extends ItemSearchResultListElementComponent {
public constructor(protected truncatableService: TruncatableService, protected dsoNameService: DSONameService) { public constructor(
super(truncatableService, dsoNameService); protected truncatableService: TruncatableService,
protected dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig: AppConfig
) {
super(truncatableService, dsoNameService, appConfig);
} }
/** /**
* Return the person name * Display thumbnail if required by configuration
*/ */
get name() { showThumbnails: boolean;
return this.dsoNameService.getName(this.dso);
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
} }
} }

View File

@@ -1,19 +1,35 @@
<ds-truncatable [id]="dso.id"> <div class="row">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" rel="noopener noreferrer"
[innerHTML]="dsoTitle"></a> [routerLink]="[itemPageRoute]" class="dont-break-out">
<span *ngIf="linkType == linkTypes.None" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
class="lead item-list-title dont-break-out" [defaultImage]="'assets/images/project-placeholder.svg'"
[innerHTML]="dsoTitle"></span> [alt]="'thumbnail.project.alt'"
<!--<span class="text-muted">--> [placeholder]="'thumbnail.project.placeholder'">
<!--<ds-truncatable-part [id]="dso.id" [minLines]="1">--> </ds-thumbnail>
<!--<span *ngIf="dso.allMetadata(['project.identifier.status']).length > 0"--> </a>
<!--class="item-list-status">--> </div>
<!--<span *ngFor="let value of allMetadataValues(['project.identifier.status']); let last=last;">--> <div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<!--<span [innerHTML]="value"><span [innerHTML]="value"></span></span>--> <ds-truncatable [id]="dso.id">
<!--</span>--> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<!--</span>--> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
<!--</ds-truncatable-part>--> rel="noopener noreferrer"
<!--</span>--> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
</ds-truncatable> [innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span>
<!--<span class="text-muted">-->
<!--<ds-truncatable-part [id]="dso.id" [minLines]="1">-->
<!--<span *ngIf="dso.allMetadata(['project.identifier.status']).length > 0"-->
<!--class="item-list-status">-->
<!--<span *ngFor="let value of allMetadataValues(['project.identifier.status']); let last=last;">-->
<!--<span [innerHTML]="value"><span [innerHTML]="value"></span></span>-->
<!--</span>-->
<!--</span>-->
<!--</ds-truncatable-part>-->
<!--</span>-->
</ds-truncatable>
</div>
</div>

View File

@@ -8,6 +8,8 @@ import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { By } from '@angular/platform-browser';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
let projectListElementComponent: ProjectSearchResultListElementComponent; let projectListElementComponent: ProjectSearchResultListElementComponent;
let fixture: ComponentFixture<ProjectSearchResultListElementComponent>; let fixture: ComponentFixture<ProjectSearchResultListElementComponent>;
@@ -50,13 +52,26 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(
}) })
}); });
const environmentUseThumbs = {
browseBy: {
showThumbnails: true
}
};
const enviromentNoThumbs = {
browseBy: {
showThumbnails: false
}
};
describe('ProjectSearchResultListElementComponent', () => { describe('ProjectSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ProjectSearchResultListElementComponent, TruncatePipe], declarations: [ProjectSearchResultListElementComponent, TruncatePipe],
providers: [ providers: [
{ provide: TruncatableService, useValue: {} }, { provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock } { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -71,6 +86,21 @@ describe('ProjectSearchResultListElementComponent', () => {
})); }));
describe('with environment.browseBy.showThumbnails set to true', () => {
beforeEach(() => {
projectListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should set showThumbnails to true', () => {
expect(projectListElementComponent.showThumbnails).toBeTrue();
});
it('should add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeTruthy();
});
});
// describe('When the item has a status', () => { // describe('When the item has a status', () => {
// beforeEach(() => { // beforeEach(() => {
// projectListElementComponent.item = mockItemWithMetadata; // projectListElementComponent.item = mockItemWithMetadata;
@@ -95,3 +125,40 @@ describe('ProjectSearchResultListElementComponent', () => {
// }); // });
// }); // });
}); });
describe('ProjectSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProjectSearchResultListElementComponent, TruncatePipe],
providers: [
{provide: TruncatableService, useValue: {}},
{provide: DSONameService, useClass: DSONameServiceMock},
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ProjectSearchResultListElementComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default}
}).compileComponents();
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(ProjectSearchResultListElementComponent);
projectListElementComponent = fixture.componentInstance;
}));
describe('with environment.browseBy.showThumbnails set to false', () => {
beforeEach(() => {
projectListElementComponent.object = mockItemWithMetadata;
fixture.detectChanges();
});
it('should not add thumbnail element', () => {
const thumbnailElement = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnailElement).toBeFalsy();
});
});
});

View File

@@ -13,4 +13,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
* The component for displaying a list element for an item search result of the type Project * The component for displaying a list element for an item search result of the type Project
*/ */
export class ProjectSearchResultListElementComponent extends ItemSearchResultListElementComponent { export class ProjectSearchResultListElementComponent extends ItemSearchResultListElementComponent {
/**
* Display thumbnail if required by configuration
*/
showThumbnails: boolean;
ngOnInit(): void {
super.ngOnInit();
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
}
} }

View File

@@ -8,6 +8,11 @@ const object = Object.assign(new ItemSearchResult(), {
indexableObject: Object.assign(new Item(), { indexableObject: Object.assign(new Item(), {
id: 'test-item', id: 'test-item',
metadata: { metadata: {
'dspace.entity.type': [
{
value: 'OrgUnit'
}
],
'organization.legalName': [ 'organization.legalName': [
{ {
value: 'title' value: 'title'

View File

@@ -17,12 +17,6 @@ import { Item } from '../../../../../core/shared/item.model';
* a sidebar search modal * a sidebar search modal
*/ */
export class OrgUnitSidebarSearchListElementComponent extends SidebarSearchListElementComponent<ItemSearchResult, Item> { export class OrgUnitSidebarSearchListElementComponent extends SidebarSearchListElementComponent<ItemSearchResult, Item> {
/**
* Get the title of the Org Unit by returning its legal name
*/
getTitle(): string {
return this.firstMetadataValue('organization.legalName');
}
/** /**
* Get the description of the Org Unit by returning its dc.description * Get the description of the Org Unit by returning its dc.description

View File

@@ -3,12 +3,16 @@ import { Collection } from '../../../../../core/shared/collection.model';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { createSidebarSearchListElementTests } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec'; import { createSidebarSearchListElementTests } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec';
import { PersonSidebarSearchListElementComponent } from './person-sidebar-search-list-element.component'; import { PersonSidebarSearchListElementComponent } from './person-sidebar-search-list-element.component';
import { TranslateService } from '@ngx-translate/core';
const object = Object.assign(new ItemSearchResult(), { const object = Object.assign(new ItemSearchResult(), {
indexableObject: Object.assign(new Item(), { indexableObject: Object.assign(new Item(), {
id: 'test-item', id: 'test-item',
metadata: { metadata: {
'dspace.entity.type': [
{
value: 'Person',
}
],
'person.familyName': [ 'person.familyName': [
{ {
value: 'family name' value: 'family name'
@@ -40,6 +44,5 @@ const parent = Object.assign(new Collection(), {
describe('PersonSidebarSearchListElementComponent', describe('PersonSidebarSearchListElementComponent',
createSidebarSearchListElementTests(PersonSidebarSearchListElementComponent, object, parent, 'parent title', 'family name, given name', 'job title', [ createSidebarSearchListElementTests(PersonSidebarSearchListElementComponent, object, parent, 'parent title', 'family name, given name', 'job title', [
{ provide: TranslateService, useValue: jasmine.createSpyObj('translate', { instant: '' }) }
]) ])
); );

View File

@@ -30,25 +30,6 @@ export class PersonSidebarSearchListElementComponent extends SidebarSearchListEl
super(truncatableService, linkService, dsoNameService); super(truncatableService, linkService, dsoNameService);
} }
/**
* Get the title of the Person by returning a combination of its family name and given name (or "No name found")
*/
getTitle(): string {
const familyName = this.firstMetadataValue('person.familyName');
const givenName = this.firstMetadataValue('person.givenName');
let title = '';
if (isNotEmpty(familyName)) {
title = familyName;
}
if (isNotEmpty(title)) {
title += ', ';
}
if (isNotEmpty(givenName)) {
title += givenName;
}
return this.defaultIfEmpty(title, this.translateService.instant('person.listelement.no-title'));
}
/** /**
* Get the description of the Person by returning its job title(s) * Get the description of the Person by returning its job title(s)
*/ */

View File

@@ -1,7 +1,6 @@
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<h2 class="item-page-title-field mr-auto"> <ds-item-page-title-field [item]="object" class="mr-auto">
{{'orgunit.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['organization.legalName'])"></ds-metadata-values> </ds-item-page-title-field>
</h2>
<div class="pl-2 space-children-mr"> <div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object" <ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'" [tooltipMsgCreate]="'item.page.version.create'"
@@ -36,7 +35,7 @@
[label]="'orgunit.page.id'"> [label]="'orgunit.page.id'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-7">
<ds-related-items <ds-related-items
[parentItem]="object" [parentItem]="object"
[relationType]="'isPublicationOfOrgUnit'" [relationType]="'isPublicationOfOrgUnit'"

View File

@@ -1,7 +1,6 @@
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<h2 class="item-page-title-field mr-auto"> <ds-item-page-title-field class="mr-auto" [item]="object">
{{'person.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="getTitleMetadataValues()" [separator]="', '"></ds-metadata-values> </ds-item-page-title-field>
</h2>
<div class="pl-2 space-children-mr"> <div class="pl-2 space-children-mr">
<ds-dso-page-orcid-button [pageRoute]="itemPageRoute" [dso]="object" class="mr-2"></ds-dso-page-orcid-button> <ds-dso-page-orcid-button [pageRoute]="itemPageRoute" [dso]="object" class="mr-2"></ds-dso-page-orcid-button>
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object" <ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
@@ -29,7 +28,7 @@
[label]="'person.page.birthdate'"> [label]="'person.page.birthdate'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-7">
<ds-related-items <ds-related-items
[parentItem]="object" [parentItem]="object"
[relationType]="'isProjectOfPerson'" [relationType]="'isProjectOfPerson'"

View File

@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component'; import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
import { MetadataValue } from '../../../../core/shared/metadata.models';
@listableObjectComponent('Person', ViewMode.StandalonePage) @listableObjectComponent('Person', ViewMode.StandalonePage)
@Component({ @Component({
@@ -14,25 +13,4 @@ import { MetadataValue } from '../../../../core/shared/metadata.models';
* The component for displaying metadata and relations of an item of the type Person * The component for displaying metadata and relations of an item of the type Person
*/ */
export class PersonComponent extends VersionedItemComponent { export class PersonComponent extends VersionedItemComponent {
/**
* Returns the metadata values to be used for the page title.
*/
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) {
metadataValues.push(familyName);
}
if (givenName) {
metadataValues.push(givenName);
}
if (metadataValues.length === 0 && title) {
metadataValues.push(title);
}
return metadataValues;
}
} }

View File

@@ -1,7 +1,6 @@
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<h2 class="item-page-title-field mr-auto"> <ds-item-page-title-field [item]="object" class="mr-auto">
{{'project.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values> </ds-item-page-title-field>
</h2>
<div class="pl-2 space-children-mr"> <div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object" <ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'" [tooltipMsgCreate]="'item.page.version.create'"
@@ -42,7 +41,7 @@
<!--[label]="'project.page.expectedcompletion'">--> <!--[label]="'project.page.expectedcompletion'">-->
<!--</ds-generic-item-page-field>--> <!--</ds-generic-item-page-field>-->
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-7">
<ds-related-items <ds-related-items
[parentItem]="object" [parentItem]="object"
[relationType]="'isPersonOfProject'" [relationType]="'isPersonOfProject'"

View File

@@ -6,9 +6,7 @@
<ds-org-unit-input-suggestions *ngIf="useNameVariants" [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)" <ds-org-unit-input-suggestions *ngIf="useNameVariants" [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)"
(submitSuggestion)="selectCustom($event)"></ds-org-unit-input-suggestions> (submitSuggestion)="selectCustom($event)"></ds-org-unit-input-suggestions>
<div *ngIf="!useNameVariants" <div *ngIf="!useNameVariants" class="lead" [innerHTML]="dsoTitle"></div>
class="lead"
[innerHTML]="firstMetadataValue('organization.legalName')"></div>
<span class="text-muted"> <span class="text-muted">
<span *ngIf="dso.allMetadata('organization.address.addressLocality').length > 0" <span *ngIf="dso.allMetadata('organization.address.addressLocality').length > 0"

View File

@@ -29,6 +29,8 @@ import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
import { OrgUnitSearchResultListSubmissionElementComponent } from './org-unit-search-result-list-submission-element.component'; import { OrgUnitSearchResultListSubmissionElementComponent } from './org-unit-search-result-list-submission-element.component';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
let personListElementComponent: OrgUnitSearchResultListSubmissionElementComponent; let personListElementComponent: OrgUnitSearchResultListSubmissionElementComponent;
let fixture: ComponentFixture<OrgUnitSearchResultListSubmissionElementComponent>; let fixture: ComponentFixture<OrgUnitSearchResultListSubmissionElementComponent>;
@@ -117,7 +119,8 @@ describe('OrgUnitSearchResultListSubmissionElementComponent', () => {
{ provide: DSOChangeAnalyzer, useValue: {} }, { provide: DSOChangeAnalyzer, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: DSONameService, useClass: DSONameServiceMock } { provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environment }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -179,4 +182,6 @@ describe('OrgUnitSearchResultListSubmissionElementComponent', () => {
expect(jobTitleField).toBeNull(); expect(jobTitleField).toBeNull();
}); });
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
@@ -17,6 +17,7 @@ import { ItemDataService } from '../../../../../core/data/item-data.service';
import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service';
import { NameVariantModalComponent } from '../../name-variant-modal/name-variant-modal.component'; import { NameVariantModalComponent } from '../../name-variant-modal/name-variant-modal.component';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModal) @listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModal)
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants) @listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants)
@@ -35,6 +36,11 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
alternativeField = 'dc.title.alternative'; alternativeField = 'dc.title.alternative';
useNameVariants = false; useNameVariants = false;
/**
* Display thumbnail if required by configuration
*/
showThumbnails: boolean;
constructor(protected truncatableService: TruncatableService, constructor(protected truncatableService: TruncatableService,
private relationshipService: RelationshipDataService, private relationshipService: RelationshipDataService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
@@ -43,9 +49,10 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
private itemDataService: ItemDataService, private itemDataService: ItemDataService,
private bitstreamDataService: BitstreamDataService, private bitstreamDataService: BitstreamDataService,
private selectableListService: SelectableListService, private selectableListService: SelectableListService,
protected dsoNameService: DSONameService protected dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig: AppConfig
) { ) {
super(truncatableService, dsoNameService); super(truncatableService, dsoNameService, appConfig);
} }
ngOnInit() { ngOnInit() {
@@ -54,7 +61,7 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
this.useNameVariants = this.context === Context.EntitySearchModalWithNameVariants; this.useNameVariants = this.context === Context.EntitySearchModalWithNameVariants;
if (this.useNameVariants) { if (this.useNameVariants) {
const defaultValue = this.firstMetadataValue('organization.legalName'); const defaultValue = this.dsoTitle;
const alternatives = this.allMetadataValues(this.alternativeField); const alternatives = this.allMetadataValues(this.alternativeField);
this.allSuggestions = [defaultValue, ...alternatives]; this.allSuggestions = [defaultValue, ...alternatives];
@@ -65,6 +72,7 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
} }
); );
} }
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
} }
select(value) { select(value) {

View File

@@ -1,6 +1,20 @@
<div class="d-flex"> <div class="row">
<div class="flex-grow-1"> <div *ngIf="showThumbnails" class="col-3 col-md-2">
<ds-person-input-suggestions [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)" (submitSuggestion)="selectCustom($event)"></ds-person-input-suggestions> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
rel="noopener noreferrer" class="dont-break-out">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/person-placeholder.svg'"
[alt]="'thumbnail.person.alt'"
[placeholder]="'thumbnail.person.placeholder'">
</ds-thumbnail>
</a>
</div>
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
<div class="d-flex">
<div class="flex-grow-1">
<ds-person-input-suggestions [suggestions]="allSuggestions" [(ngModel)]="selectedName"
(clickSuggestion)="select($event)"
(submitSuggestion)="selectCustom($event)"></ds-person-input-suggestions>
<span class="text-muted"> <span class="text-muted">
<span *ngIf="dso.allMetadata(['person.jobTitle']).length > 0" <span *ngIf="dso.allMetadata(['person.jobTitle']).length > 0"
class="item-list-job-title"> class="item-list-job-title">
@@ -9,5 +23,7 @@
</span> </span>
</span> </span>
</span> </span>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -27,6 +27,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
import { PersonSearchResultListSubmissionElementComponent } from './person-search-result-list-submission-element.component'; import { PersonSearchResultListSubmissionElementComponent } from './person-search-result-list-submission-element.component';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
let personListElementComponent: PersonSearchResultListSubmissionElementComponent; let personListElementComponent: PersonSearchResultListSubmissionElementComponent;
let fixture: ComponentFixture<PersonSearchResultListSubmissionElementComponent>; let fixture: ComponentFixture<PersonSearchResultListSubmissionElementComponent>;
@@ -37,6 +38,18 @@ let mockItemWithoutMetadata: ItemSearchResult;
let nameVariant; let nameVariant;
let mockRelationshipService; let mockRelationshipService;
const environmentUseThumbs = {
browseBy: {
showThumbnails: true
}
};
const enviromentNoThumbs = {
browseBy: {
showThumbnails: false
}
};
function init() { function init() {
mockItemWithMetadata = Object.assign( mockItemWithMetadata = Object.assign(
new ItemSearchResult(), new ItemSearchResult(),
@@ -109,6 +122,7 @@ describe('PersonSearchResultListElementSubmissionComponent', () => {
{ provide: DSOChangeAnalyzer, useValue: {} }, { provide: DSOChangeAnalyzer, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -146,4 +160,72 @@ describe('PersonSearchResultListElementSubmissionComponent', () => {
expect(jobTitleField).toBeNull(); expect(jobTitleField).toBeNull();
}); });
}); });
describe('When the environment is set to show thumbnails', () => {
beforeEach(() => {
personListElementComponent.object = mockItemWithoutMetadata;
fixture.detectChanges();
});
it('should add the ds-thumbnail element', () => {
const thumbnail = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnail).toBeTruthy();
});
});
});
describe('PersonSearchResultListElementSubmissionComponent', () => {
const mockBitstreamDataService = {
getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
return createSuccessfulRemoteDataObject$(new Bitstream());
}
};
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule({
declarations: [PersonSearchResultListSubmissionElementComponent, TruncatePipe],
providers: [
{ provide: TruncatableService, useValue: {} },
{ provide: RelationshipDataService, useValue: mockRelationshipService },
{ provide: NotificationsService, useValue: {} },
{ provide: TranslateService, useValue: {} },
{ provide: NgbModal, useValue: {} },
{ provide: ItemDataService, useValue: {} },
{ provide: SelectableListService, useValue: {} },
{ provide: Store, useValue: {}},
{ provide: ObjectCacheService, useValue: {} },
{ provide: UUIDService, useValue: {} },
{ provide: RemoteDataBuildService, useValue: {} },
{ provide: CommunityDataService, useValue: {} },
{ provide: HALEndpointService, useValue: {} },
{ provide: HttpClient, useValue: {} },
{ provide: DSOChangeAnalyzer, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(PersonSearchResultListSubmissionElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(PersonSearchResultListSubmissionElementComponent);
personListElementComponent = fixture.componentInstance;
}));
describe('When the environment is not set to show thumbnails', () => {
beforeEach(() => {
personListElementComponent.object = mockItemWithoutMetadata;
fixture.detectChanges();
});
it('should not add the ds-thumbnail element', () => {
const thumbnail = fixture.debugElement.query(By.css('ds-thumbnail'));
expect(thumbnail).toBeNull();
});
});
}); });

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, Inject, OnInit } from '@angular/core';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
@@ -17,6 +17,7 @@ import { MetadataValue } from '../../../../../core/shared/metadata.models';
import { ItemDataService } from '../../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../../core/data/item-data.service';
import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants) @listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants)
@Component({ @Component({
@@ -33,6 +34,11 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu
selectedName: string; selectedName: string;
alternativeField = 'dc.title.alternative'; alternativeField = 'dc.title.alternative';
/**
* Display thumbnail if required by configuration
*/
showThumbnails: boolean;
constructor(protected truncatableService: TruncatableService, constructor(protected truncatableService: TruncatableService,
private relationshipService: RelationshipDataService, private relationshipService: RelationshipDataService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
@@ -41,14 +47,15 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu
private itemDataService: ItemDataService, private itemDataService: ItemDataService,
private bitstreamDataService: BitstreamDataService, private bitstreamDataService: BitstreamDataService,
private selectableListService: SelectableListService, private selectableListService: SelectableListService,
protected dsoNameService: DSONameService protected dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig: AppConfig
) { ) {
super(truncatableService, dsoNameService); super(truncatableService, dsoNameService, appConfig);
} }
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
const defaultValue = this.firstMetadataValue('person.familyName') + ', ' + this.firstMetadataValue('person.givenName'); const defaultValue = this.dsoTitle;
const alternatives = this.allMetadataValues(this.alternativeField); const alternatives = this.allMetadataValues(this.alternativeField);
this.allSuggestions = [defaultValue, ...alternatives]; this.allSuggestions = [defaultValue, ...alternatives];
@@ -58,6 +65,7 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu
this.selectedName = nameVariant || defaultValue; this.selectedName = nameVariant || defaultValue;
} }
); );
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
} }
select(value) { select(value) {

View File

@@ -1,4 +1,4 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { RouterStub } from '../../shared/testing/router.stub'; import { RouterStub } from '../../shared/testing/router.stub';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
@@ -108,6 +108,7 @@ describe('ForgotPasswordFormComponent', () => {
expect(router.navigate).not.toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled();
expect(notificationsService.error).toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled();
}); });
it('should submit a patch request for the user uuid when the form is invalid', () => { it('should submit a patch request for the user uuid when the form is invalid', () => {
comp.password = 'password'; comp.password = 'password';

View File

@@ -10,10 +10,7 @@ import { AuthenticateAction } from '../../core/auth/auth.actions';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, } from '../../core/shared/operators';
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload,
} from '../../core/shared/operators';
import { CoreState } from '../../core/core-state.model'; import { CoreState } from '../../core/core-state.model';
@Component({ @Component({

View File

@@ -10,6 +10,9 @@ import { StatisticsModule } from '../statistics/statistics.module';
import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component'; import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component';
import { ThemedHomePageComponent } from './themed-home-page.component'; import { ThemedHomePageComponent } from './themed-home-page.component';
import { RecentItemListComponent } from './recent-item-list/recent-item-list.component'; import { RecentItemListComponent } from './recent-item-list/recent-item-list.component';
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
const DECLARATIONS = [ const DECLARATIONS = [
HomePageComponent, HomePageComponent,
ThemedHomePageComponent, ThemedHomePageComponent,
@@ -22,7 +25,9 @@ const DECLARATIONS = [
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule.withEntryComponents(),
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(),
HomePageRoutingModule, HomePageRoutingModule,
StatisticsModule.forRoot() StatisticsModule.forRoot()
], ],

View File

@@ -1,5 +1,5 @@
<ng-container *ngVar="(itemRD$ | async) as itemRD"> <ng-container *ngVar="(itemRD$ | async) as itemRD">
<div class="mt-4" *ngIf="itemRD?.hasSucceeded && itemRD?.payload?.page.length > 0" @fadeIn> <div class="mt-4" [ngClass]="placeholderFontClass" *ngIf="itemRD?.hasSucceeded && itemRD?.payload?.page.length > 0" @fadeIn>
<div class="d-flex flex-row border-bottom mb-4 pb-4 ng-tns-c416-2"></div> <div class="d-flex flex-row border-bottom mb-4 pb-4 ng-tns-c416-2"></div>
<h2> {{'home.recent-submissions.head' | translate}}</h2> <h2> {{'home.recent-submissions.head' | translate}}</h2>
<div class="my-4" *ngFor="let item of itemRD?.payload?.page"> <div class="my-4" *ngFor="let item of itemRD?.payload?.page">

View File

@@ -10,8 +10,11 @@ import { SearchConfigurationService } from '../../core/shared/search/search-conf
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { ViewMode } from 'src/app/core/shared/view-mode.model';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { APP_CONFIG } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
import { PLATFORM_ID } from '@angular/core';
describe('RecentItemListComponent', () => { describe('RecentItemListComponent', () => {
let component: RecentItemListComponent; let component: RecentItemListComponent;
let fixture: ComponentFixture<RecentItemListComponent>; let fixture: ComponentFixture<RecentItemListComponent>;
@@ -42,6 +45,8 @@ describe('RecentItemListComponent', () => {
{ provide: SearchService, useValue: searchServiceStub }, { provide: SearchService, useValue: searchServiceStub },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub }, { provide: SearchConfigurationService, useValue: searchConfigServiceStub },
{ provide: APP_CONFIG, useValue: environment },
{ provide: PLATFORM_ID, useValue: 'browser' },
], ],
}) })
.compileComponents(); .compileComponents();

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { fadeIn, fadeInOut } from '../../shared/animations/fade';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
@@ -11,12 +11,13 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { import { toDSpaceObjectListRD } from '../../core/shared/operators';
toDSpaceObjectListRD import { Observable } from 'rxjs';
} from '../../core/shared/operators'; import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
Observable, import { isPlatformBrowser } from '@angular/common';
} from 'rxjs'; import { setPlaceHolderAttributes } from '../../shared/utils/object-list-utils';
@Component({ @Component({
selector: 'ds-recent-item-list', selector: 'ds-recent-item-list',
templateUrl: './recent-item-list.component.html', templateUrl: './recent-item-list.component.html',
@@ -31,14 +32,22 @@ export class RecentItemListComponent implements OnInit {
itemRD$: Observable<RemoteData<PaginatedList<Item>>>; itemRD$: Observable<RemoteData<PaginatedList<Item>>>;
paginationConfig: PaginationComponentOptions; paginationConfig: PaginationComponentOptions;
sortConfig: SortOptions; sortConfig: SortOptions;
/** /**
* The view-mode we're currently on * The view-mode we're currently on
* @type {ViewMode} * @type {ViewMode}
*/ */
viewMode = ViewMode.ListElement; viewMode = ViewMode.ListElement;
constructor(private searchService: SearchService,
private _placeholderFontClass: string;
constructor(
private searchService: SearchService,
private paginationService: PaginationService, private paginationService: PaginationService,
public searchConfigurationService: SearchConfigurationService public searchConfigurationService: SearchConfigurationService,
protected elementRef: ElementRef,
@Inject(APP_CONFIG) private appConfig: AppConfig,
@Inject(PLATFORM_ID) private platformId: Object,
) { ) {
this.paginationConfig = Object.assign(new PaginationComponentOptions(), { this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
@@ -50,16 +59,29 @@ export class RecentItemListComponent implements OnInit {
this.sortConfig = new SortOptions(environment.homePage.recentSubmissions.sortField, SortDirection.DESC); this.sortConfig = new SortOptions(environment.homePage.recentSubmissions.sortField, SortDirection.DESC);
} }
ngOnInit(): void { ngOnInit(): void {
const linksToFollow: FollowLinkConfig<Item>[] = [];
if (this.appConfig.browseBy.showThumbnails) {
linksToFollow.push(followLink('thumbnail'));
}
this.itemRD$ = this.searchService.search( this.itemRD$ = this.searchService.search(
new PaginatedSearchOptions({ new PaginatedSearchOptions({
pagination: this.paginationConfig, pagination: this.paginationConfig,
sort: this.sortConfig, sort: this.sortConfig,
}), }),
).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>; undefined,
undefined,
undefined,
...linksToFollow,
).pipe(
toDSpaceObjectListRD()
) as Observable<RemoteData<PaginatedList<Item>>>;
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.paginationService.clearPagination(this.paginationConfig.id); this.paginationService.clearPagination(this.paginationConfig.id);
} }
onLoadMore(): void { onLoadMore(): void {
this.paginationService.updateRouteWithUrl(this.searchConfigurationService.paginationID, ['search'], { this.paginationService.updateRouteWithUrl(this.searchConfigurationService.paginationID, ['search'], {
sortField: environment.homePage.recentSubmissions.sortField, sortField: environment.homePage.recentSubmissions.sortField,
@@ -68,5 +90,17 @@ export class RecentItemListComponent implements OnInit {
}); });
} }
get placeholderFontClass(): string {
if (this._placeholderFontClass === undefined) {
if (isPlatformBrowser(this.platformId)) {
const width = this.elementRef.nativeElement.offsetWidth;
this._placeholderFontClass = setPlaceHolderAttributes(width);
} else {
this._placeholderFontClass = 'hide-placeholder-text';
}
}
return this._placeholderFontClass;
}
} }

View File

@@ -32,6 +32,8 @@ import { SearchConfigurationService } from '../../core/shared/search/search-conf
import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
import { APP_CONFIG } from 'src/config/app-config.interface';
import { environment } from 'src/environments/environment.test';
describe('TopLevelCommunityList Component', () => { describe('TopLevelCommunityList Component', () => {
let comp: TopLevelCommunityListComponent; let comp: TopLevelCommunityListComponent;
@@ -151,6 +153,7 @@ describe('TopLevelCommunityList Component', () => {
], ],
declarations: [TopLevelCommunityListComponent], declarations: [TopLevelCommunityListComponent],
providers: [ providers: [
{ provide: APP_CONFIG, useValue: environment },
{ provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit, OnDestroy, Inject } from '@angular/core';
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs'; import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
@@ -12,6 +12,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
/** /**
* this component renders the Top-Level Community list * this component renders the Top-Level Community list
@@ -50,11 +51,14 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy {
*/ */
currentPageSubscription: Subscription; currentPageSubscription: Subscription;
constructor(private cds: CommunityDataService, constructor(
private paginationService: PaginationService) { @Inject(APP_CONFIG) protected appConfig: AppConfig,
private cds: CommunityDataService,
private paginationService: PaginationService
) {
this.config = new PaginationComponentOptions(); this.config = new PaginationComponentOptions();
this.config.id = this.pageId; this.config.id = this.pageId;
this.config.pageSize = 5; this.config.pageSize = appConfig.homePage.topLevelCommunityList.pageSize;
this.config.currentPage = 1; this.config.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
} }

View File

@@ -31,6 +31,7 @@ import { SearchConfigurationServiceStub } from '../../../../shared/testing/searc
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RouterMock } from '../../../../shared/mocks/router.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock';
import { APP_CONFIG } from '../../../../../config/app-config.interface';
let comp: EditRelationshipListComponent; let comp: EditRelationshipListComponent;
let fixture: ComponentFixture<EditRelationshipListComponent>; let fixture: ComponentFixture<EditRelationshipListComponent>;
@@ -201,6 +202,12 @@ describe('EditRelationshipListComponent', () => {
})) }))
}); });
const environmentUseThumbs = {
browseBy: {
showThumbnails: true
}
};
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [SharedModule, TranslateModule.forRoot()], imports: [SharedModule, TranslateModule.forRoot()],
declarations: [EditRelationshipListComponent], declarations: [EditRelationshipListComponent],
@@ -217,6 +224,7 @@ describe('EditRelationshipListComponent', () => {
{ provide: LinkHeadService, useValue: linkHeadService }, { provide: LinkHeadService, useValue: linkHeadService },
{ provide: ConfigurationDataService, useValue: configurationDataService }, { provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
], schemas: [ ], schemas: [
NO_ERRORS_SCHEMA NO_ERRORS_SCHEMA
] ]
@@ -259,9 +267,11 @@ describe('EditRelationshipListComponent', () => {
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
const findListOptions = callArgs[2]; const findListOptions = callArgs[2];
const linksToFollow = callArgs[5];
expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize); expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize);
expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage); expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage);
expect(linksToFollow.linksToFollow[0].name).toEqual('thumbnail');
}); });
describe('when the publication is on the left side of the relationship', () => { describe('when the publication is on the left side of the relationship', () => {

Some files were not shown because too many files have changed in this diff Show More