Merge branch 'master' of github.com:DSpace/dspace-angular into configurable_entities

This commit is contained in:
Paulo Graça
2019-12-10 18:05:54 +00:00
70 changed files with 2172 additions and 264 deletions

View File

@@ -75,6 +75,7 @@
},
"dependencies": {
"@angular/animations": "^6.1.4",
"@angular/cdk": "^6.4.7",
"@angular/cli": "^6.1.5",
"@angular/common": "^6.1.4",
"@angular/core": "^6.1.4",

View File

@@ -340,6 +340,14 @@
"communityList.tabTitle": "DSpace - Community List",
"communityList.title": "List of Communities",
"communityList.showMore": "Show More",
"community.create.head": "Create a Community",
"community.create.sub-head": "Create a Sub-Community for Community {{ parent }}",
@@ -818,6 +826,14 @@
"item.page.related-items.view-less": "View less",
"item.page.relationships.isAuthorOfPublication": "Publications",
"item.page.relationships.isJournalOfPublication": "Publications",
"item.page.relationships.isOrgUnitOfPerson": "Authors",
"item.page.relationships.isOrgUnitOfProject": "Research Projects",
"item.page.subject": "Keywords",
"item.page.uri": "URI",
@@ -1268,6 +1284,8 @@
"project.page.titleprefix": "Research Project: ",
"project.search.results.head": "Project Search Results",
"publication.listelement.badge": "Publication",

View File

@@ -5,7 +5,7 @@ import { PaginatedList } from '../../../core/data/paginated-list';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
import { FindAllOptions } from '../../../core/data/request.models';
import { FindListOptions } from '../../../core/data/request.models';
import { map, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
@@ -35,7 +35,7 @@ export class BitstreamFormatsComponent implements OnInit {
* The current pagination configuration for the page used by the FindAll method
* Currently simply renders all bitstream formats
*/
config: FindAllOptions = Object.assign(new FindAllOptions(), {
config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20
});
@@ -145,7 +145,7 @@ export class BitstreamFormatsComponent implements OnInit {
* @param event The page change event
*/
onPageChange(event) {
this.config = Object.assign(new FindAllOptions(), this.config, {
this.config = Object.assign(new FindListOptions(), this.config, {
currentPage: event,
});
this.pageConfig.currentPage = event;

View File

@@ -26,6 +26,7 @@ import { MetadataRepresentationListComponent } from './simple/metadata-represent
import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component';
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component';
import { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component';
import { StatisticsModule } from '../statistics/statistics.module';
@NgModule({
@@ -55,7 +56,8 @@ import { StatisticsModule } from '../statistics/statistics.module';
ItemComponent,
GenericItemPageFieldComponent,
MetadataRepresentationListComponent,
RelatedEntitiesSearchComponent
RelatedEntitiesSearchComponent,
TabbedRelatedEntitiesSearchComponent
],
exports: [
ItemComponent,
@@ -65,7 +67,8 @@ import { StatisticsModule } from '../statistics/statistics.module';
RelatedEntitiesSearchComponent,
RelatedItemsComponent,
MetadataRepresentationListComponent,
ItemPageTitleFieldComponent
ItemPageTitleFieldComponent,
TabbedRelatedEntitiesSearchComponent
],
entryComponents: [
PublicationComponent

View File

@@ -1,6 +1,7 @@
<ds-filtered-search-page
<ds-configuration-search-page
[fixedFilterQuery]="fixedFilter"
[configuration]="configuration"
[configuration$]="configuration$"
[searchEnabled]="searchEnabled"
[sideBarWidth]="sideBarWidth">
</ds-filtered-search-page>
</ds-configuration-search-page>

View File

@@ -16,7 +16,7 @@ describe('RelatedEntitiesSearchComponent', () => {
id: 'id1'
});
const mockRelationType = 'publicationsOfAuthor';
const mockRelationEntityType = 'publication';
const mockConfiguration = 'publication';
const mockFilter= `f.${mockRelationType}=${mockItem.id}`;
const fixedFilterServiceStub = {
getFilterByRelation: () => mockFilter
@@ -39,7 +39,7 @@ describe('RelatedEntitiesSearchComponent', () => {
fixedFilterService = (comp as any).fixedFilterService;
comp.relationType = mockRelationType;
comp.item = mockItem;
comp.relationEntityType = mockRelationEntityType;
comp.configuration = mockConfiguration;
fixture.detectChanges();
});
@@ -49,7 +49,7 @@ describe('RelatedEntitiesSearchComponent', () => {
it('should create a configuration$', () => {
comp.configuration$.subscribe((configuration) => {
expect(configuration).toEqual(mockRelationEntityType);
expect(configuration).toEqual(mockConfiguration);
})
});

View File

@@ -22,18 +22,16 @@ export class RelatedEntitiesSearchComponent implements OnInit {
*/
@Input() relationType: string;
/**
* An optional configuration to use for the search options
*/
@Input() configuration: string;
/**
* The item to render relationships for
*/
@Input() item: Item;
/**
* The entity type of the relationship items to be displayed
* e.g. 'publication'
* This determines the title of the search results (if search is enabled)
*/
@Input() relationEntityType: string;
/**
* Whether or not the search bar and title should be displayed (defaults to true)
* @type {boolean}
@@ -56,8 +54,8 @@ export class RelatedEntitiesSearchComponent implements OnInit {
if (isNotEmpty(this.relationType) && isNotEmpty(this.item)) {
this.fixedFilter = this.fixedFilterService.getFilterByRelation(this.relationType, this.item.id);
}
if (isNotEmpty(this.relationEntityType)) {
this.configuration$ = of(this.relationEntityType);
if (isNotEmpty(this.configuration)) {
this.configuration$ = of(this.configuration);
}
}

View File

@@ -0,0 +1,22 @@
<ngb-tabset *ngIf="relationTypes.length > 1" [destroyOnHide]="true" #tabs="ngbTabset" [activeId]="activeTab$ | async" (tabChange)="onTabChange($event)">
<ngb-tab *ngFor="let relationType of relationTypes" title="{{'item.page.relationships.' + relationType.label | translate}}" [id]="relationType.filter">
<ng-template ngbTabContent>
<div class="mt-4">
<ds-related-entities-search [item]="item"
[relationType]="relationType.filter"
[configuration]="relationType.configuration"
[searchEnabled]="searchEnabled"
[sideBarWidth]="sideBarWidth">
</ds-related-entities-search>
</div>
</ng-template>
</ngb-tab>
</ngb-tabset>
<div *ngIf="relationTypes.length === 1" class="mt-4">
<ds-related-entities-search *ngVar="relationTypes[0] as relationType" [item]="item"
[relationType]="relationType.filter"
[configuration]="relationType.configuration"
[searchEnabled]="searchEnabled"
[sideBarWidth]="sideBarWidth">
</ds-related-entities-search>
</div>

View File

@@ -0,0 +1,82 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Item } from '../../../../core/shared/item.model';
import { TranslateModule } from '@ngx-translate/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TabbedRelatedEntitiesSearchComponent } from './tabbed-related-entities-search.component';
import { ActivatedRoute, Router } from '@angular/router';
import { MockRouter } from '../../../../shared/mocks/mock-router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { VarDirective } from '../../../../shared/utils/var.directive';
import { of as observableOf } from 'rxjs';
describe('TabbedRelatedEntitiesSearchComponent', () => {
let comp: TabbedRelatedEntitiesSearchComponent;
let fixture: ComponentFixture<TabbedRelatedEntitiesSearchComponent>;
const mockItem = Object.assign(new Item(), {
id: 'id1'
});
const mockRelationType = 'publications';
const relationTypes = [
{
label: mockRelationType,
filter: mockRelationType
}
];
const router = new MockRouter();
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, NgbModule.forRoot()],
declarations: [TabbedRelatedEntitiesSearchComponent, VarDirective],
providers: [
{
provide: ActivatedRoute,
useValue: {
queryParams: observableOf({ tab: mockRelationType })
},
},
{ provide: Router, useValue: router }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TabbedRelatedEntitiesSearchComponent);
comp = fixture.componentInstance;
comp.item = mockItem;
comp.relationTypes = relationTypes;
fixture.detectChanges();
});
it('should initialize the activeTab depending on the current query parameters', () => {
comp.activeTab$.subscribe((activeTab) => {
expect(activeTab).toEqual(mockRelationType);
});
});
describe('onTabChange', () => {
const event = {
currentId: mockRelationType,
nextId: 'nextTab'
};
beforeEach(() => {
comp.onTabChange(event);
});
it('should call router natigate with the correct arguments', () => {
expect(router.navigate).toHaveBeenCalledWith([], {
relativeTo: (comp as any).route,
queryParams: {
tab: event.nextId
},
queryParamsHandling: 'merge'
});
});
});
});

View File

@@ -0,0 +1,76 @@
import { Component, Input, OnInit } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs/internal/Observable';
import { map } from 'rxjs/operators';
@Component({
selector: 'ds-tabbed-related-entities-search',
templateUrl: './tabbed-related-entities-search.component.html'
})
/**
* A component to show related items as search results, split into tabs by relationship-type
* Related items can be facetted, or queried using an
* optional search box.
*/
export class TabbedRelatedEntitiesSearchComponent implements OnInit {
/**
* The types of relationships to fetch items for
* e.g. 'isAuthorOfPublication'
*/
@Input() relationTypes: Array<{
label: string,
filter: string,
configuration?: string
}>;
/**
* The item to render relationships for
*/
@Input() item: Item;
/**
* Whether or not the search bar and title should be displayed (defaults to true)
* @type {boolean}
*/
@Input() searchEnabled = true;
/**
* The ratio of the sidebar's width compared to the search results (1-12) (defaults to 4)
* @type {number}
*/
@Input() sideBarWidth = 4;
/**
* The active tab
*/
activeTab$: Observable<string>;
constructor(private route: ActivatedRoute,
private router: Router) {
}
/**
* If the url contains a "tab" query parameter, set this tab to be the active tab
*/
ngOnInit(): void {
this.activeTab$ = this.route.queryParams.pipe(
map((params) => params.tab)
);
}
/**
* Add a "tab" query parameter to the URL when changing tabs
* @param event
*/
onTabChange(event) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
tab: event.nextId
},
queryParamsHandling: 'merge'
});
}
}

View File

@@ -4,7 +4,7 @@ import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { RelationshipService } from '../../../core/data/relationship.service';
import { FindAllOptions } from '../../../core/data/request.models';
import { FindListOptions } from '../../../core/data/request.models';
import { Subscription } from 'rxjs/internal/Subscription';
import { ViewMode } from '../../../core/shared/view-mode.model';
@@ -33,7 +33,7 @@ export class RelatedItemsComponent implements OnInit, OnDestroy {
* Default options to start a search request with
* Optional input, should you wish a different page size (or other options)
*/
@Input() options = Object.assign(new FindAllOptions(), { elementsPerPage: 5 });
@Input() options = Object.assign(new FindListOptions(), { elementsPerPage: 5 });
/**
* An i18n label to use as a title for the list (usually describes the relation)
@@ -53,7 +53,7 @@ export class RelatedItemsComponent implements OnInit, OnDestroy {
/**
* Search options for displaying all elements in a list
*/
allOptions = Object.assign(new FindAllOptions(), { elementsPerPage: 9999 });
allOptions = Object.assign(new FindListOptions(), { elementsPerPage: 9999 });
/**
* The view-mode we're currently on

View File

@@ -35,6 +35,12 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements
*/
@Input() configuration: string;
/**
* The actual query for the fixed filter.
* If empty, the query will be determined by the route parameter called 'filter'
*/
@Input() fixedFilterQuery: string;
constructor(protected service: SearchService,
protected sidebarService: SidebarService,
protected windowService: HostWindowService,
@@ -64,7 +70,11 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements
return this.searchConfigService.paginatedSearchOptions.pipe(
map((options: PaginatedSearchOptions) => {
const config = this.configuration || options.configuration;
return Object.assign(options, { configuration: config });
const filter = this.fixedFilterQuery || options.fixedFilter;
return Object.assign(options, {
configuration: config,
fixedFilter: filter
});
})
);
}

View File

@@ -1,21 +0,0 @@
import { FilteredSearchPageComponent } from './filtered-search-page.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { configureSearchComponentTestingModule } from './search.component.spec';
import { SearchConfigurationService } from './search-service/search-configuration.service';
describe('FilteredSearchPageComponent', () => {
let comp: FilteredSearchPageComponent;
let fixture: ComponentFixture<FilteredSearchPageComponent>;
let searchConfigService: SearchConfigurationService;
beforeEach(async(() => {
configureSearchComponentTestingModule(FilteredSearchPageComponent);
}));
beforeEach(() => {
fixture = TestBed.createComponent(FilteredSearchPageComponent);
comp = fixture.componentInstance;
searchConfigService = (comp as any).searchConfigService;
fixture.detectChanges();
});
});

View File

@@ -1,73 +0,0 @@
import { HostWindowService } from '../shared/host-window.service';
import { SearchService } from './search-service/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchComponent } from './search.component';
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
import { pushInOut } from '../shared/animations/push';
import { SearchConfigurationService } from './search-service/search-configuration.service';
import { Observable } from 'rxjs';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { map } from 'rxjs/operators';
import { RouteService } from '../core/services/route.service';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-filtered-search-page',
styleUrls: ['./search.component.scss'],
templateUrl: './search.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [pushInOut],
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: SearchConfigurationService
}
]
})
export class FilteredSearchPageComponent extends SearchComponent implements OnInit {
/**
* The actual query for the fixed filter.
* If empty, the query will be determined by the route parameter called 'filter'
*/
@Input() fixedFilterQuery: string;
constructor(protected service: SearchService,
protected sidebarService: SidebarService,
protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService) {
super(service, sidebarService, windowService, searchConfigService, routeService);
}
/**
* Listening to changes in the paginated search options
* If something changes, update the search results
*
* Listen to changes in the scope
* If something changes, update the list of scopes for the dropdown
*/
ngOnInit(): void {
super.ngOnInit();
}
/**
* Get the current paginated search options after updating the fixed filter using the fixedFilterQuery input
* This is to make sure the fixed filter is included in the paginated search options, as it is not part of any
* query or route parameters
* @returns {Observable<PaginatedSearchOptions>}
*/
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
return this.searchConfigService.paginatedSearchOptions.pipe(
map((options: PaginatedSearchOptions) => {
const filter = this.fixedFilterQuery || options.fixedFilter;
return Object.assign(options, { fixedFilter: filter });
})
);
}
}

View File

@@ -32,7 +32,6 @@ import { SearchAuthorityFilterComponent } from './search-filters/search-filter/s
import { SearchLabelComponent } from './search-labels/search-label/search-label.component';
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { FilteredSearchPageComponent } from './filtered-search-page.component';
import { SearchPageComponent } from './search-page.component';
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
import { StatisticsModule } from '../statistics/statistics.module';
@@ -64,7 +63,6 @@ const components = [
SearchFacetRangeOptionComponent,
SearchSwitchConfigurationComponent,
SearchAuthorityFilterComponent,
FilteredSearchPageComponent,
ConfigurationSearchPageComponent,
SearchTrackerComponent,
];

View File

@@ -27,6 +27,7 @@ export function getAdminModulePath() {
RouterModule.forRoot([
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },

View File

@@ -1,5 +1,6 @@
import { ActionReducerMap, createSelector, MemoizedSelector, State } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { formReducer, FormState } from './shared/form/form.reducer';
import {
@@ -48,6 +49,7 @@ export interface AppState {
cssVariables: CSSVariablesState;
menus: MenusState;
objectSelection: ObjectSelectionListState;
communityList: CommunityListState;
}
export const appReducers: ActionReducerMap<AppState> = {
@@ -64,7 +66,8 @@ export const appReducers: ActionReducerMap<AppState> = {
truncatable: truncatableReducer,
cssVariables: cssVariablesReducer,
menus: menusReducer,
objectSelection: objectSelectionReducer
objectSelection: objectSelectionReducer,
communityList: CommunityListReducer,
};
export const routerStateSelector = (state: AppState) => state.router;

View File

@@ -0,0 +1,40 @@
import { CommunityListService, FlatNode } from './community-list-service';
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
import { BehaviorSubject, Observable, } from 'rxjs';
import { finalize, take, } from 'rxjs/operators';
/**
* DataSource object needed by a CDK Tree to render its nodes.
* The list of FlatNodes that this DataSource object represents gets created in the CommunityListService at
* the beginning (initial page-limited top communities) and re-calculated any time the tree state changes
* (a node gets expanded or page-limited result become larger by triggering a show more node)
*/
export class CommunityListDatasource implements DataSource<FlatNode> {
private communityList$ = new BehaviorSubject<FlatNode[]>([]);
public loading$ = new BehaviorSubject<boolean>(false);
constructor(private communityListService: CommunityListService) {
}
connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
return this.communityList$.asObservable();
}
loadCommunities(expandedNodes: FlatNode[]) {
this.loading$.next(true);
this.communityListService.loadCommunities(expandedNodes).pipe(
take(1),
finalize(() => this.loading$.next(false)),
).subscribe((flatNodes: FlatNode[]) => {
this.communityList$.next(flatNodes);
});
}
disconnect(collectionViewer: CollectionViewer): void {
this.communityList$.complete();
this.loading$.complete();
}
}

View File

@@ -0,0 +1,4 @@
<div class="container">
<h2>{{ 'communityList.title' | translate }}</h2>
<ds-community-list></ds-community-list>
</div>

View File

@@ -0,0 +1,41 @@
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { CommunityListPageComponent } from './community-list-page.component';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
describe('CommunityListPageComponent', () => {
let component: CommunityListPageComponent;
let fixture: ComponentFixture<CommunityListPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
},
}),
],
declarations: [CommunityListPageComponent],
providers: [
CommunityListPageComponent,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CommunityListPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', inject([CommunityListPageComponent], (comp: CommunityListPageComponent) => {
expect(comp).toBeTruthy();
}));
});

View File

@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
/**
* Page with title and the community list tree, as described in community-list.component;
* navigated to with community-list.page.routing.module
*/
@Component({
selector: 'ds-community-list-page',
templateUrl: './community-list-page.component.html',
})
export class CommunityListPageComponent {
}

View File

@@ -0,0 +1,26 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { CommunityListPageComponent } from './community-list-page.component';
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
import { CommunityListComponent } from './community-list/community-list.component';
import { CdkTreeModule } from '@angular/cdk/tree';
/**
* The page which houses a title and the community list, as described in community-list.component
*/
@NgModule({
imports: [
CommonModule,
SharedModule,
CommunityListPageRoutingModule,
CdkTreeModule,
],
declarations: [
CommunityListPageComponent,
CommunityListComponent
]
})
export class CommunityListPageModule {
}

View File

@@ -0,0 +1,26 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CdkTreeModule } from '@angular/cdk/tree';
import { CommunityListPageComponent } from './community-list-page.component';
import { CommunityListService } from './community-list-service';
/**
* RouterModule to help navigate to the page with the community list tree
*/
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: CommunityListPageComponent,
pathMatch: 'full',
data: { title: 'communityList.tabTitle' }
}
]),
CdkTreeModule,
],
providers: [CommunityListService]
})
export class CommunityListPageRoutingModule {
}

View File

@@ -0,0 +1,574 @@
import { of as observableOf } from 'rxjs';
import { TestBed, inject, async } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { AppState } from '../app.reducer';
import { MockStore } from '../shared/testing/mock-store';
import { CommunityListService, FlatNode, toFlatNode } from './community-list-service';
import { CollectionDataService } from '../core/data/collection-data.service';
import { PaginatedList } from '../core/data/paginated-list';
import { PageInfo } from '../core/shared/page-info.model';
import { CommunityDataService } from '../core/data/community-data.service';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$
} from '../shared/testing/utils';
import { Community } from '../core/shared/community.model';
import { Collection } from '../core/shared/collection.model';
import { take } from 'rxjs/operators';
import { FindListOptions } from '../core/data/request.models';
describe('CommunityListService', () => {
let store: MockStore<AppState>;
const standardElementsPerPage = 2;
let collectionDataServiceStub: any;
let communityDataServiceStub: any;
const mockSubcommunities1Page1 = [Object.assign(new Community(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
}),
Object.assign(new Community(), {
id: '59ee713b-ee53-4220-8c3f-9860dc84fe33',
uuid: '59ee713b-ee53-4220-8c3f-9860dc84fe33',
})
];
const mockCollectionsPage1 = [
Object.assign(new Collection(), {
id: 'e9dbf393-7127-415f-8919-55be34a6e9ed',
uuid: 'e9dbf393-7127-415f-8919-55be34a6e9ed',
name: 'Collection 1'
}),
Object.assign(new Collection(), {
id: '59da2ff0-9bf4-45bf-88be-e35abd33f304',
uuid: '59da2ff0-9bf4-45bf-88be-e35abd33f304',
name: 'Collection 2'
})
];
const mockCollectionsPage2 = [
Object.assign(new Collection(), {
id: 'a5159760-f362-4659-9e81-e3253ad91ede',
uuid: 'a5159760-f362-4659-9e81-e3253ad91ede',
name: 'Collection 3'
}),
Object.assign(new Collection(), {
id: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3',
uuid: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3',
name: 'Collection 4'
})
];
const mockListOfTopCommunitiesPage1 = [
Object.assign(new Community(), {
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
}),
Object.assign(new Community(), {
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])),
}),
Object.assign(new Community(), {
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
}),
];
const mockListOfTopCommunitiesPage2 = [
Object.assign(new Community(), {
id: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6',
uuid: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
}),
];
const mockTopCommunitiesWithChildrenArraysPage1 = [
{
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
subcommunities: mockSubcommunities1Page1,
collections: [],
},
{
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
subcommunities: [],
collections: [...mockCollectionsPage1, ...mockCollectionsPage2],
},
{
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
subcommunities: [],
collections: [],
}];
const mockTopCommunitiesWithChildrenArraysPage2 = [
{
id: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6',
uuid: 'c2e04392-3b8a-4dfa-976d-d76fb1b8a4b6',
subcommunities: [],
collections: [],
}];
const allCommunities = [...mockTopCommunitiesWithChildrenArraysPage1, ...mockTopCommunitiesWithChildrenArraysPage2, ...mockSubcommunities1Page1];
let service: CommunityListService;
beforeEach(async(() => {
communityDataServiceStub = {
findTop(options: FindListOptions = {}) {
const allTopComs = [...mockListOfTopCommunitiesPage1, ...mockListOfTopCommunitiesPage2];
let currentPage = options.currentPage;
const elementsPerPage = 3;
if (currentPage === undefined) {
currentPage = 1
}
const startPageIndex = (currentPage - 1) * elementsPerPage;
let endPageIndex = (currentPage * elementsPerPage);
if (endPageIndex > allTopComs.length) {
endPageIndex = allTopComs.length;
}
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allTopComs.slice(startPageIndex, endPageIndex)));
},
findByParent(parentUUID: string, options: FindListOptions = {}) {
const foundCom = allCommunities.find((community) => (community.id === parentUUID));
let currentPage = options.currentPage;
let elementsPerPage = options.elementsPerPage;
if (currentPage === undefined) {
currentPage = 1
}
if (elementsPerPage === 0) {
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), (foundCom.subcommunities as [Community])));
}
elementsPerPage = standardElementsPerPage;
if (foundCom !== undefined && foundCom.subcommunities !== undefined) {
const coms = foundCom.subcommunities as [Community];
const startPageIndex = (currentPage - 1) * elementsPerPage;
let endPageIndex = (currentPage * elementsPerPage);
if (endPageIndex > coms.length) {
endPageIndex = coms.length;
}
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), coms.slice(startPageIndex, endPageIndex)));
} else {
return createFailedRemoteDataObject$();
}
}
};
collectionDataServiceStub = {
findByParent(parentUUID: string, options: FindListOptions = {}) {
const foundCom = allCommunities.find((community) => (community.id === parentUUID));
let currentPage = options.currentPage;
let elementsPerPage = options.elementsPerPage;
if (currentPage === undefined) {
currentPage = 1
}
if (elementsPerPage === 0) {
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), (foundCom.collections as [Collection])));
}
elementsPerPage = standardElementsPerPage;
if (foundCom !== undefined && foundCom.collections !== undefined) {
const colls = foundCom.collections as [Collection];
const startPageIndex = (currentPage - 1) * elementsPerPage;
let endPageIndex = (currentPage * elementsPerPage);
if (endPageIndex > colls.length) {
endPageIndex = colls.length;
}
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), colls.slice(startPageIndex, endPageIndex)));
} else {
return createFailedRemoteDataObject$();
}
}
};
TestBed.configureTestingModule({
providers: [CommunityListService,
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: Store, useValue: MockStore },
],
});
store = TestBed.get(Store);
service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store);
}));
afterAll(() => service = new CommunityListService(communityDataServiceStub, collectionDataServiceStub, store));
it('should create', inject([CommunityListService], (serviceIn: CommunityListService) => {
expect(serviceIn).toBeTruthy();
}));
describe('getNextPageTopCommunities', () => {
describe('also load in second page of top communities', () => {
let flatNodeList;
describe('None expanded: should return list containing only flatnodes of the test top communities page 1 and 2', () => {
let findTopSpy;
beforeEach(() => {
findTopSpy = spyOn(communityDataServiceStub, 'findTop').and.callThrough();
service.getNextPageTopCommunities();
const sub = service.loadCommunities(null)
.subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('flatnode list should contain just flatnodes of top community list page 1 and 2', () => {
expect(findTopSpy).toHaveBeenCalled();
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockListOfTopCommunitiesPage2.length);
mockListOfTopCommunitiesPage1.map((community) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === community.id))).toBeTruthy();
});
mockListOfTopCommunitiesPage2.map((community) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === community.id))).toBeTruthy();
});
});
});
});
});
describe('loadCommunities', () => {
describe('should transform all communities in a list of flatnodes with possible subcoms and collections as subflatnodes if they\'re expanded', () => {
let flatNodeList;
describe('None expanded: should return list containing only flatnodes of the test top communities', () => {
beforeEach(() => {
const sub = service.loadCommunities(null)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('length of flatnode list should be as big as top community list', () => {
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length);
});
it('flatnode list should contain flatNode representations of top communities', () => {
mockListOfTopCommunitiesPage1.map((community) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === community.id))).toBeTruthy();
});
});
it('none of the flatnodes in the list should be expanded', () => {
flatNodeList.map((flatnode: FlatNode) => {
expect(flatnode.isExpanded).toEqual(false);
});
});
});
describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
beforeEach(() => {
const expandedNodes = [];
mockListOfTopCommunitiesPage1.map((community: Community) => {
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
communityFlatNode.currentCollectionPage = 1;
communityFlatNode.currentCommunityPage = 1;
expandedNodes.push(communityFlatNode);
});
const sub = service.loadCommunities(expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('length of flatnode list should be as big as top community list and size of its possible page-limited children', () => {
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
});
it('flatnode list should contain flatNode representations of all page-limited children', () => {
mockSubcommunities1Page1.map((subcommunity) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === subcommunity.id))).toBeTruthy();
});
mockCollectionsPage1.map((collection) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === collection.id))).toBeTruthy();
});
});
});
describe('Just first top comm expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
beforeEach(() => {
const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[0], observableOf(true), 0, true, null);
communityFlatNode.currentCollectionPage = 1;
communityFlatNode.currentCommunityPage = 1;
const expandedNodes = [communityFlatNode];
const sub = service.loadCommunities(expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('length of flatnode list should be as big as top community list and size of page-limited children of first top community', () => {
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length);
});
it('flatnode list should contain flatNode representations of all page-limited children of first top community', () => {
mockSubcommunities1Page1.map((subcommunity) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === subcommunity.id))).toBeTruthy();
});
});
});
describe('Just second top comm expanded, collections at page 2: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
beforeEach(() => {
const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[1], observableOf(true), 0, true, null);
communityFlatNode.currentCollectionPage = 2;
communityFlatNode.currentCommunityPage = 1;
const expandedNodes = [communityFlatNode];
const sub = service.loadCommunities(expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('length of flatnode list should be as big as top community list and size of page-limited children of second top community', () => {
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockCollectionsPage1.length + mockCollectionsPage2.length);
});
it('flatnode list should contain flatNode representations of all page-limited children of first top community', () => {
mockCollectionsPage1.map((collection) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === collection.id))).toBeTruthy();
});
mockCollectionsPage2.map((collection) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === collection.id))).toBeTruthy();
});
});
});
});
});
describe('transformListOfCommunities', () => {
describe('should transform list of communities in a list of flatnodes with possible subcoms and collections as subflatnodes if they\'re expanded', () => {
describe('list of communities with possible children', () => {
const listOfCommunities = mockListOfTopCommunitiesPage1;
let flatNodeList;
describe('None expanded: should return list containing only flatnodes of the communities in the test list', () => {
beforeEach(() => {
const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('length of flatnode list should be as big as community test list', () => {
expect(flatNodeList.length).toEqual(listOfCommunities.length);
});
it('flatnode list should contain flatNode representations of all communities from test list', () => {
listOfCommunities.map((community) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === community.id))).toBeTruthy();
});
});
it('none of the flatnodes in the list should be expanded', () => {
flatNodeList.map((flatnode: FlatNode) => {
expect(flatnode.isExpanded).toEqual(false);
});
});
});
describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
beforeEach(() => {
const expandedNodes = [];
listOfCommunities.map((community: Community) => {
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
communityFlatNode.currentCollectionPage = 1;
communityFlatNode.currentCommunityPage = 1;
expandedNodes.push(communityFlatNode);
});
const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('length of flatnode list should be as big as community test list and size of its possible children', () => {
expect(flatNodeList.length).toEqual(listOfCommunities.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
});
it('flatnode list should contain flatNode representations of all children', () => {
mockSubcommunities1Page1.map((subcommunity) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === subcommunity.id))).toBeTruthy();
});
mockSubcommunities1Page1.map((collection) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === collection.id))).toBeTruthy();
});
});
});
});
});
});
describe('transformCommunity', () => {
describe('should transform community in list of flatnodes with possible subcoms and collections as subflatnodes if its expanded', () => {
describe('topcommunity without subcoms or collections, unexpanded', () => {
const communityWithNoSubcomsOrColls = Object.assign(new Community(), {
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: {
'dc.description': [{ language: 'en_US', value: 'no subcoms, 2 coll' }],
'dc.title': [{ language: 'en_US', value: 'Community 2' }]
}
});
let flatNodeList;
describe('should return list containing only flatnode corresponding to that community', () => {
beforeEach(() => {
const sub = service.transformCommunity(communityWithNoSubcomsOrColls, 0, null, null)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('length of flatnode list should be 1', () => {
expect(flatNodeList.length).toEqual(1);
});
it('flatnode list only element should be flatNode of test community', () => {
expect(flatNodeList[0].id).toEqual(communityWithNoSubcomsOrColls.id);
});
it('flatnode from test community is not expanded', () => {
expect(flatNodeList[0].isExpanded).toEqual(false);
});
});
});
describe('topcommunity with subcoms or collections, unexpanded', () => {
const communityWithSubcoms = Object.assign(new Community(), {
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: {
'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }],
'dc.title': [{ language: 'en_US', value: 'Community 1' }]
}
});
let flatNodeList;
describe('should return list containing only flatnode corresponding to that community', () => {
beforeAll(() => {
const sub = service.transformCommunity(communityWithSubcoms, 0, null, null)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('length of flatnode list should be 1', () => {
expect(flatNodeList.length).toEqual(1);
});
it('flatnode list only element should be flatNode of test community', () => {
expect(flatNodeList[0].id).toEqual(communityWithSubcoms.id);
});
it('flatnode from test community is not expanded', () => {
expect(flatNodeList[0].isExpanded).toEqual(false);
});
});
});
describe('topcommunity with subcoms, expanded, first page for all', () => {
describe('should return list containing flatnodes of that community, its possible subcommunities and its possible collections', () => {
const communityWithSubcoms = Object.assign(new Community(), {
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: {
'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }],
'dc.title': [{ language: 'en_US', value: 'Community 1' }]
}
});
let flatNodeList;
beforeEach(() => {
const communityFlatNode = toFlatNode(communityWithSubcoms, observableOf(true), 0, true, null);
communityFlatNode.currentCollectionPage = 1;
communityFlatNode.currentCommunityPage = 1;
const expandedNodes = [communityFlatNode];
const sub = service.transformCommunity(communityWithSubcoms, 0, null, expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('list of flatnodes is length is 1 + nrOfSubcoms & first flatnode is of expanded test community', () => {
expect(flatNodeList.length).toEqual(1 + mockSubcommunities1Page1.length);
expect(flatNodeList[0].isExpanded).toEqual(true);
expect(flatNodeList[0].id).toEqual(communityWithSubcoms.id);
});
it('list of flatnodes contains flatnodes for all subcoms of test community', () => {
mockSubcommunities1Page1.map((subcommunity) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === subcommunity.id))).toBeTruthy();
});
});
it('the subcoms of the test community are a level higher than the parent community', () => {
mockSubcommunities1Page1.map((subcommunity) => {
expect((flatNodeList.find((flatnode) => (flatnode.id === subcommunity.id))).level).toEqual(flatNodeList[0].level + 1);
});
});
});
});
describe('topcommunity with collections, expanded, on second page of collections', () => {
describe('should return list containing flatnodes of that community, its collections of the first two pages', () => {
const communityWithCollections = Object.assign(new Community(), {
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])),
metadata: {
'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }],
'dc.title': [{ language: 'en_US', value: 'Community 1' }]
}
});
let flatNodeList;
beforeEach(() => {
const communityFlatNode = toFlatNode(communityWithCollections, observableOf(true), 0, true, null);
communityFlatNode.currentCollectionPage = 2;
communityFlatNode.currentCommunityPage = 1;
const expandedNodes = [communityFlatNode];
const sub = service.transformCommunity(communityWithCollections, 0, null, expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value);
sub.unsubscribe();
});
it('list of flatnodes is length is 1 + nrOfCollections & first flatnode is of expanded test community', () => {
expect(flatNodeList.length).toEqual(1 + mockCollectionsPage1.length + mockCollectionsPage2.length);
expect(flatNodeList[0].isExpanded).toEqual(true);
expect(flatNodeList[0].id).toEqual(communityWithCollections.id);
});
it('list of flatnodes contains flatnodes for all subcolls (first 2 pages) of test community', () => {
mockCollectionsPage1.map((collection) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === collection.id))).toBeTruthy();
});
mockCollectionsPage2.map((collection) => {
expect(flatNodeList.find((flatnode) => (flatnode.id === collection.id))).toBeTruthy();
})
});
it('the collections of the test community are a level higher than the parent community', () => {
mockCollectionsPage1.map((collection) => {
expect((flatNodeList.find((flatnode) => (flatnode.id === collection.id))).level).toEqual(flatNodeList[0].level + 1);
});
mockCollectionsPage2.map((collection) => {
expect((flatNodeList.find((flatnode) => (flatnode.id === collection.id))).level).toEqual(flatNodeList[0].level + 1);
})
});
});
});
});
});
describe('getIsExpandable', () => {
describe('should return true', () => {
it('if community has subcommunities', () => {
const communityWithSubcoms = Object.assign(new Community(), {
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: {
'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }],
'dc.title': [{ language: 'en_US', value: 'Community 1' }]
}
});
service.getIsExpandable(communityWithSubcoms).pipe(take(1)).subscribe((result) => {
expect(result).toEqual(true);
});
});
it('if community has collections', () => {
const communityWithCollections = Object.assign(new Community(), {
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockCollectionsPage1)),
metadata: {
'dc.description': [{ language: 'en_US', value: 'no subcoms, 2 coll' }],
'dc.title': [{ language: 'en_US', value: 'Community 2' }]
}
});
service.getIsExpandable(communityWithCollections).pipe(take(1)).subscribe((result) => {
expect(result).toEqual(true);
});
});
});
describe('should return false', () => {
it('if community has neither subcommunities nor collections', () => {
const communityWithNoSubcomsOrColls = Object.assign(new Community(), {
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: {
'dc.description': [{ language: 'en_US', value: 'no subcoms, no coll' }],
'dc.title': [{ language: 'en_US', value: 'Community 3' }]
}
});
service.getIsExpandable(communityWithNoSubcomsOrColls).pipe(take(1)).subscribe((result) => {
expect(result).toEqual(false);
});
});
});
});
});

View File

@@ -0,0 +1,335 @@
import { Injectable } from '@angular/core';
import { createSelector, Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
import { Observable, of as observableOf } from 'rxjs';
import { AppState } from '../app.reducer';
import { CommunityDataService } from '../core/data/community-data.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { catchError, filter, map, switchMap, take } from 'rxjs/operators';
import { Community } from '../core/shared/community.model';
import { Collection } from '../core/shared/collection.model';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { RemoteData } from '../core/data/remote-data';
import { PaginatedList } from '../core/data/paginated-list';
import { getCommunityPageRoute } from '../+community-page/community-page-routing.module';
import { getCollectionPageRoute } from '../+collection-page/collection-page-routing.module';
import { CollectionDataService } from '../core/data/collection-data.service';
import { CommunityListSaveAction } from './community-list.actions';
import { CommunityListState } from './community-list.reducer';
/**
* Each node in the tree is represented by a flatNode which contains info about the node itself and its position and
* state in the tree. There are nodes representing communities, collections and show more links.
*/
export interface FlatNode {
isExpandable$: Observable<boolean>;
name: string;
id: string;
level: number;
isExpanded?: boolean;
parent?: FlatNode;
payload: Community | Collection | ShowMoreFlatNode;
isShowMoreNode: boolean;
route?: string;
currentCommunityPage?: number;
currentCollectionPage?: number;
}
/**
* The show more links in the community tree are also represented by a flatNode so we know where in
* the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link)
*/
export class ShowMoreFlatNode {
}
// Helper method to combine an flatten an array of observables of flatNode arrays
export const combineAndFlatten = (obsList: Array<Observable<FlatNode[]>>): Observable<FlatNode[]> =>
observableCombineLatest(...obsList).pipe(
map((matrix: FlatNode[][]) =>
matrix.reduce((combinedList, currentList: FlatNode[]) => [...combinedList, ...currentList]))
);
/**
* Creates a flatNode from a community or collection
* @param c The community or collection this flatNode represents
* @param isExpandable Whether or not this node is expandable (true if it has children)
* @param level Level indicating how deep in the tree this node should be rendered
* @param isExpanded Whether or not this node already is expanded
* @param parent Parent of this node (flatNode representing its parent community)
*/
export const toFlatNode = (
c: Community | Collection,
isExpandable: Observable<boolean>,
level: number,
isExpanded: boolean,
parent?: FlatNode
): FlatNode => ({
isExpandable$: isExpandable,
name: c.name,
id: c.id,
level: level,
isExpanded,
parent,
payload: c,
isShowMoreNode: false,
route: c instanceof Community ? getCommunityPageRoute(c.id) : getCollectionPageRoute(c.id),
});
/**
* Creates a show More flatnode where only the level and parent are of importance
*/
export const showMoreFlatNode = (
id: string,
level: number,
parent: FlatNode
): FlatNode => ({
isExpandable$: observableOf(false),
name: 'Show More Flatnode',
id: id,
level: level,
isExpanded: false,
parent: parent,
payload: new ShowMoreFlatNode(),
isShowMoreNode: true,
});
// Selectors the get the communityList data out of the store
const communityListStateSelector = (state: AppState) => state.communityList;
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
/**
* 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
*/
// tslint:disable-next-line:max-classes-per-file
@Injectable()
export class CommunityListService {
// page-limited list of top-level communities
payloads$: Array<Observable<PaginatedList<Community>>>;
topCommunitiesConfig: PaginationComponentOptions;
topCommunitiesSortConfig: SortOptions;
maxSubCommunitiesPerPage: number;
maxCollectionsPerPage: number;
constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService,
private store: Store<any>) {
this.topCommunitiesConfig = new PaginationComponentOptions();
this.topCommunitiesConfig.id = 'top-level-pagination';
this.topCommunitiesConfig.pageSize = 10;
this.topCommunitiesConfig.currentPage = 1;
this.topCommunitiesSortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.initTopCommunityList();
this.maxSubCommunitiesPerPage = 3;
this.maxCollectionsPerPage = 3;
}
saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void {
this.store.dispatch(new CommunityListSaveAction(expandedNodes, loadingNode));
}
getExpandedNodesFromStore(): Observable<FlatNode[]> {
return this.store.select(expandedNodesSelector);
}
getLoadingNodeFromStore(): Observable<FlatNode> {
return this.store.select(loadingNodeSelector);
}
/**
* Increases the payload so it contains the next page of top level communities
*/
getNextPageTopCommunities(): void {
this.topCommunitiesConfig.currentPage = this.topCommunitiesConfig.currentPage + 1;
this.payloads$ = [...this.payloads$, this.communityDataService.findTop({
currentPage: this.topCommunitiesConfig.currentPage,
elementsPerPage: this.topCommunitiesConfig.pageSize,
sort: {
field: this.topCommunitiesSortConfig.field,
direction: this.topCommunitiesSortConfig.direction
}
}).pipe(
take(1),
map((results) => results.payload),
)];
}
/**
* Gets all top communities, limited by page, and transforms this in a list of flatNodes.
* @param expandedNodes List of expanded nodes; if a node is not expanded its subCommunities and collections need
* not be added to the list
*/
loadCommunities(expandedNodes: FlatNode[]): Observable<FlatNode[]> {
const res = this.payloads$.map((payload) => {
return payload.pipe(
take(1),
switchMap((result: PaginatedList<Community>) => {
return this.transformListOfCommunities(result, 0, null, expandedNodes);
}),
catchError(() => observableOf([])),
);
});
return combineAndFlatten(res);
};
/**
* Puts the initial top level communities in a list to be called upon
*/
private initTopCommunityList(): void {
this.payloads$ = [this.communityDataService.findTop({
currentPage: this.topCommunitiesConfig.currentPage,
elementsPerPage: this.topCommunitiesConfig.pageSize,
sort: {
field: this.topCommunitiesSortConfig.field,
direction: this.topCommunitiesSortConfig.direction
}
}).pipe(
take(1),
map((results) => results.payload),
)];
}
/**
* Transforms a list of communities to a list of FlatNodes according to the instructions detailed in transformCommunity
* @param listOfPaginatedCommunities Paginated list of communities to be transformed
* @param level Level the tree is currently at
* @param parent FlatNode of the parent of this list of communities
* @param expandedNodes List of expanded nodes; if a node is not expanded its subcommunities and collections need not be added to the list
*/
public transformListOfCommunities(listOfPaginatedCommunities: PaginatedList<Community>,
level: number,
parent: FlatNode,
expandedNodes: FlatNode[]): Observable<FlatNode[]> {
if (isNotEmpty(listOfPaginatedCommunities.page)) {
let currentPage = this.topCommunitiesConfig.currentPage;
if (isNotEmpty(parent)) {
currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage;
}
const isNotAllCommunities = (listOfPaginatedCommunities.totalElements > (listOfPaginatedCommunities.elementsPerPage * currentPage));
let obsList = listOfPaginatedCommunities.page
.map((community: Community) => {
return this.transformCommunity(community, level, parent, expandedNodes)
});
if (isNotAllCommunities && listOfPaginatedCommunities.currentPage > currentPage) {
obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])];
}
return combineAndFlatten(obsList);
} else {
return observableOf([]);
}
}
/**
* Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself,
* followed by flatNodes of its possible subcommunities and collection
* It gets called recursively for each subcommunity to add its subcommunities and collections to the list
* Number of subcommunities and collections added, is dependant on the current page the parent is at for respectively subcommunities and collections.
* @param community Community being transformed
* @param level Depth of the community in the list, subcommunities and collections go one level deeper
* @param parent Flatnode of the parent community
* @param expandedNodes List of nodes which are expanded, if node is not expanded, it need not add its page-limited subcommunities or collections
*/
public transformCommunity(community: Community, level: number, parent: FlatNode, expandedNodes: FlatNode[]): Observable<FlatNode[]> {
let isExpanded = false;
if (isNotEmpty(expandedNodes)) {
isExpanded = hasValue(expandedNodes.find((node) => (node.id === community.id)));
}
const isExpandable$ = this.getIsExpandable(community);
const communityFlatNode = toFlatNode(community, isExpandable$, level, isExpanded, parent);
let obsList = [observableOf([communityFlatNode])];
if (isExpanded) {
const currentCommunityPage = expandedNodes.find((node: FlatNode) => node.id === community.id).currentCommunityPage;
let subcoms = [];
for (let i = 1; i <= currentCommunityPage; i++) {
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
elementsPerPage: this.maxSubCommunitiesPerPage,
currentPage: i
})
.pipe(
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded),
take(1),
switchMap((rd: RemoteData<PaginatedList<Community>>) =>
this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes))
);
subcoms = [...subcoms, nextSetOfSubcommunitiesPage];
}
obsList = [...obsList, combineAndFlatten(subcoms)];
const currentCollectionPage = expandedNodes.find((node: FlatNode) => node.id === community.id).currentCollectionPage;
let collections = [];
for (let i = 1; i <= currentCollectionPage; i++) {
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
elementsPerPage: this.maxCollectionsPerPage,
currentPage: i
})
.pipe(
filter((rd: RemoteData<PaginatedList<Collection>>) => rd.hasSucceeded),
take(1),
map((rd: RemoteData<PaginatedList<Collection>>) => {
let nodes = rd.payload.page
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
if ((rd.payload.elementsPerPage * currentCollectionPage) < rd.payload.totalElements && rd.payload.currentPage > currentCollectionPage) {
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
}
return nodes;
}),
);
collections = [...collections, nextSetOfCollectionsPage];
}
obsList = [...obsList, combineAndFlatten(collections)];
}
return combineAndFlatten(obsList);
}
/**
* Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0
* Returns an observable that combines the result.payload.totalElements fo the observables that the
* respective services return when queried
* @param community Community being checked whether it is expandable (if it has subcommunities or collections)
*/
public getIsExpandable(community: Community): Observable<boolean> {
let hasSubcoms$: Observable<boolean>;
let hasColls$: Observable<boolean>;
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 })
.pipe(
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded),
take(1),
map((results) => results.payload.totalElements > 0),
);
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
.pipe(
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded),
take(1),
map((results) => results.payload.totalElements > 0),
);
let hasChildren$: Observable<boolean>;
hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe(
take(1),
map((result: [boolean]) => {
if (result[0] || result[1]) {
return true;
} else {
return false;
}
})
);
return hasChildren$;
}
}

View File

@@ -0,0 +1,35 @@
import { Action } from '@ngrx/store';
import { type } from '../shared/ngrx/type';
import { FlatNode } from './community-list-service';
/**
* All the action types of the community-list
*/
export const CommunityListActionTypes = {
SAVE: type('dspace/community-list-page/SAVE')
};
/**
* Community list SAVE action
*/
export class CommunityListSaveAction implements Action {
type = CommunityListActionTypes.SAVE;
payload: {
expandedNodes: FlatNode[];
loadingNode: FlatNode;
};
constructor(expandedNodes: FlatNode[], loadingNode: FlatNode) {
this.payload = { expandedNodes, loadingNode }
}
};
/**
* Export a type alias of all actions in this action group
* so that reducers can easily compose action types
*/
export type CommunityListActions = CommunityListSaveAction;

View File

@@ -0,0 +1,45 @@
import { of as observableOf } from 'rxjs/internal/observable/of';
import { PaginatedList } from '../core/data/paginated-list';
import { Community } from '../core/shared/community.model';
import { PageInfo } from '../core/shared/page-info.model';
import { createSuccessfulRemoteDataObject$ } from '../shared/testing/utils';
import { toFlatNode } from './community-list-service';
import { CommunityListSaveAction } from './community-list.actions';
import { CommunityListReducer } from './community-list.reducer';
describe('communityListReducer', () => {
const mockSubcommunities1Page1 = [Object.assign(new Community(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
name: 'subcommunity1',
})];
const mockFlatNodeOfCommunity = toFlatNode(
Object.assign(new Community(), {
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
name: 'community1',
}), observableOf(true), 0, false, null
);
it ('should set init state of the expandedNodes and loadingNode', () => {
const state = {
expandedNodes: [],
loadingNode: null,
};
const action = new CommunityListSaveAction([], null);
const newState = CommunityListReducer(null, action);
expect(newState).toEqual(state);
});
it ('should save new state of the expandedNodes and loadingNode at a save action', () => {
const state = {
expandedNodes: [mockFlatNodeOfCommunity],
loadingNode: null,
};
const action = new CommunityListSaveAction([mockFlatNodeOfCommunity], null);
const newState = CommunityListReducer(null, action);
expect(newState).toEqual(state);
});
});

View File

@@ -0,0 +1,36 @@
import { FlatNode } from './community-list-service';
import { CommunityListActions, CommunityListActionTypes, CommunityListSaveAction } from './community-list.actions';
/**
* States we wish to put in store concerning the community list
*/
export interface CommunityListState {
expandedNodes: FlatNode[];
loadingNode: FlatNode;
}
/**
* Initial starting state of the list of expandedNodes and the current loading node of the community list
*/
const initialState: CommunityListState = {
expandedNodes: [],
loadingNode: null,
};
/**
* Reducer to interact with store concerning objects for the community list
* @constructor
*/
export function CommunityListReducer(state = initialState, action: CommunityListActions) {
switch (action.type) {
case CommunityListActionTypes.SAVE: {
return Object.assign({}, state, {
expandedNodes: (action as CommunityListSaveAction).payload.expandedNodes,
loadingNode: (action as CommunityListSaveAction).payload.loadingNode,
})
}
default: {
return state;
}
}
}

View File

@@ -0,0 +1,91 @@
<ds-loading *ngIf="(dataSource.loading$ | async) && loadingNode === undefined " class="ds-loading"></ds-loading>
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
<!-- This is the tree node template for show more node -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
class="example-tree-node show-more-node">
<div class="btn-group">
<button type="button" class="btn btn-default" cdkTreeNodeToggle>
<span class="fa fa-chevron-right invisible" aria-hidden="true"></span>
</button>
<div class="align-middle pt-2">
<a *ngIf="node!==loadingNode" [routerLink]="" (click)="getNextPage(node)"
class="btn btn-outline-secondary btn-sm">
{{ 'communityList.showMore' | translate }}
</a>
<ds-loading *ngIf="node===loadingNode && dataSource.loading$ | async" class="ds-loading"></ds-loading>
</div>
</div>
<div class="text-muted" cdkTreeNodePadding>
<div class="d-flex">
</div>
</div>
</cdk-tree-node>
<!-- This is the tree node template for expandable nodes (coms and subcoms with children) -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChild" cdkTreeNodePadding
class="example-tree-node expandable-node">
<div class="btn-group">
<button type="button" class="btn btn-default" cdkTreeNodeToggle
[attr.aria-label]="'toggle ' + node.name"
(click)="toggleExpanded(node)"
[ngClass]="(node.isExpandable$ | async) ? 'visible' : 'invisible'">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span>
</button>
<h5 class="align-middle pt-2">
<a [routerLink]="node.route" class="lead">
{{node.name}}
</a>
</h5>
</div>
<ds-truncatable [id]="node.id">
<div class="text-muted" cdkTreeNodePadding>
<div class="d-flex" *ngIf="node.payload.shortDescription">
<button type="button" class="btn btn-default invisible">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span>
</button>
<ds-truncatable-part [id]="node.id" [minLines]="3">
<span>{{node.payload.shortDescription}}</span>
</ds-truncatable-part>
</div>
</div>
</ds-truncatable>
<div class="d-flex" *ngIf="node===loadingNode && dataSource.loading$ | async"
cdkTreeNodePadding>
<button type="button" class="btn btn-default invisible">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span>
</button>
<ds-loading class="ds-loading"></ds-loading>
</div>
</cdk-tree-node>
<!-- This is the tree node template for leaf nodes (collections and (sub)coms without children) -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: !(hasChild && isShowMore)" cdkTreeNodePadding
class="example-tree-node childless-node">
<div class="btn-group">
<button type="button" class="btn btn-default" cdkTreeNodeToggle>
<span class="fa fa-chevron-right invisible"
aria-hidden="true"></span>
</button>
<h6 class="align-middle pt-2">
<a [routerLink]="node.route" class="lead">
{{node.name}}
</a>
</h6>
</div>
<ds-truncatable [id]="node.id">
<div class="text-muted" cdkTreeNodePadding>
<div class="d-flex" *ngIf="node.payload.shortDescription">
<button type="button" class="btn btn-default invisible">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span>
</button>
<ds-truncatable-part [id]="node.id" [minLines]="3">
<span>{{node.payload.shortDescription}}</span>
</ds-truncatable-part>
</div>
</div>
</ds-truncatable>
</cdk-tree-node>
</cdk-tree>

View File

@@ -0,0 +1,336 @@
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
import { CommunityListComponent } from './community-list.component';
import {
CommunityListService,
FlatNode,
showMoreFlatNode,
toFlatNode
} from '../community-list-service';
import { CdkTreeModule } from '@angular/cdk/tree';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { Community } from '../../core/shared/community.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
import { PaginatedList } from '../../core/data/paginated-list';
import { PageInfo } from '../../core/shared/page-info.model';
import { Collection } from '../../core/shared/collection.model';
import { of as observableOf } from 'rxjs';
import { By } from '@angular/platform-browser';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
describe('CommunityListComponent', () => {
let component: CommunityListComponent;
let fixture: ComponentFixture<CommunityListComponent>;
const mockSubcommunities1Page1 = [Object.assign(new Community(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
name: 'subcommunity1',
}),
Object.assign(new Community(), {
id: '59ee713b-ee53-4220-8c3f-9860dc84fe33',
uuid: '59ee713b-ee53-4220-8c3f-9860dc84fe33',
name: 'subcommunity2',
})
];
const mockCollectionsPage1 = [
Object.assign(new Collection(), {
id: 'e9dbf393-7127-415f-8919-55be34a6e9ed',
uuid: 'e9dbf393-7127-415f-8919-55be34a6e9ed',
name: 'collection1',
}),
Object.assign(new Collection(), {
id: '59da2ff0-9bf4-45bf-88be-e35abd33f304',
uuid: '59da2ff0-9bf4-45bf-88be-e35abd33f304',
name: 'collection2',
})
];
const mockCollectionsPage2 = [
Object.assign(new Collection(), {
id: 'a5159760-f362-4659-9e81-e3253ad91ede',
uuid: 'a5159760-f362-4659-9e81-e3253ad91ede',
name: 'collection3',
}),
Object.assign(new Collection(), {
id: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3',
uuid: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3',
name: 'collection4',
})
];
const mockTopCommunitiesWithChildrenArrays = [
{
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
subcommunities: mockSubcommunities1Page1,
collections: [],
},
{
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
subcommunities: [],
collections: [...mockCollectionsPage1, ...mockCollectionsPage2],
},
{
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
subcommunities: [],
collections: [],
}];
const mockTopFlatnodesUnexpanded: FlatNode[] = [
toFlatNode(
Object.assign(new Community(), {
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockSubcommunities1Page1)),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
name: 'community1',
}), observableOf(true), 0, false, null
),
toFlatNode(
Object.assign(new Community(), {
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])),
name: 'community2',
}), observableOf(true), 0, false, null
),
toFlatNode(
Object.assign(new Community(), {
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
collections: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
name: 'community3',
}), observableOf(false), 0, false, null
),
];
let communityListServiceStub;
beforeEach(async(() => {
communityListServiceStub = {
topPageSize: 2,
topCurrentPage: 1,
collectionPageSize: 2,
subcommunityPageSize: 2,
expandedNodes: [],
loadingNode: null,
getNextPageTopCommunities() {
this.topCurrentPage++;
},
getLoadingNodeFromStore() {
return observableOf(this.loadingNode);
},
getExpandedNodesFromStore() {
return observableOf(this.expandedNodes);
},
saveCommunityListStateToStore(expandedNodes, loadingNode) {
this.expandedNodes = expandedNodes;
this.loadingNode = loadingNode;
},
loadCommunities(expandedNodes) {
let flatnodes;
let showMoreTopComNode = false;
flatnodes = [...mockTopFlatnodesUnexpanded];
const currentPage = this.topCurrentPage;
const elementsPerPage = this.topPageSize;
let endPageIndex = (currentPage * elementsPerPage);
if (endPageIndex >= flatnodes.length) {
endPageIndex = flatnodes.length;
} else {
showMoreTopComNode = true;
}
if (expandedNodes === null || isEmpty(expandedNodes)) {
if (showMoreTopComNode) {
return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode('community', 0, null)]);
} else {
return observableOf(mockTopFlatnodesUnexpanded.slice(0, endPageIndex));
}
} else {
flatnodes = [];
const topFlatnodes = mockTopFlatnodesUnexpanded.slice(0, endPageIndex);
topFlatnodes.map((topNode: FlatNode) => {
flatnodes = [...flatnodes, topNode];
const expandedParent: FlatNode = expandedNodes.find((expandedNode: FlatNode) => expandedNode.id === topNode.id);
if (isNotEmpty(expandedParent)) {
const matchingTopComWithArrays = mockTopCommunitiesWithChildrenArrays.find((topcom) => topcom.id === topNode.id);
if (isNotEmpty(matchingTopComWithArrays)) {
const possibleSubcoms: Community[] = matchingTopComWithArrays.subcommunities;
let subComFlatnodes = [];
possibleSubcoms.map((subcom: Community) => {
subComFlatnodes = [...subComFlatnodes, toFlatNode(subcom, observableOf(false), topNode.level + 1, false, topNode)];
});
const possibleColls: Collection[] = matchingTopComWithArrays.collections;
let collFlatnodes = [];
possibleColls.map((coll: Collection) => {
collFlatnodes = [...collFlatnodes, toFlatNode(coll, observableOf(false), topNode.level + 1, false, topNode)];
});
if (isNotEmpty(subComFlatnodes)) {
const endSubComIndex = this.subcommunityPageSize * expandedParent.currentCommunityPage;
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
if (subComFlatnodes.length > endSubComIndex) {
flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)];
}
}
if (isNotEmpty(collFlatnodes)) {
const endColIndex = this.collectionPageSize * expandedParent.currentCollectionPage;
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
if (collFlatnodes.length > endColIndex) {
flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)];
}
}
}
}
});
if (showMoreTopComNode) {
flatnodes = [...flatnodes, showMoreFlatNode('community', 0, null)];
}
return observableOf(flatnodes);
}
}
};
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
},
}),
CdkTreeModule,
RouterTestingModule],
declarations: [CommunityListComponent],
providers: [CommunityListComponent,
{ provide: CommunityListService, useValue: communityListServiceStub },],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CommunityListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', inject([CommunityListComponent], (comp: CommunityListComponent) => {
expect(comp).toBeTruthy();
}));
it('should render a cdk tree with the first elementsPerPage (2) nr of top level communities, unexpanded', () => {
const expandableNodesFound = fixture.debugElement.queryAll(By.css('.expandable-node a'));
const childlessNodesFound = fixture.debugElement.queryAll(By.css('.childless-node a'));
const allNodes = [...expandableNodesFound, ...childlessNodesFound];
expect(allNodes.length).toEqual(2);
mockTopFlatnodesUnexpanded.slice(0, 2).map((topFlatnode: FlatNode) => {
expect(allNodes.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === topFlatnode.name);
})).toBeTruthy();
});
});
it('show more node is present at end of nodetree', () => {
const showMoreEl = fixture.debugElement.queryAll(By.css('.show-more-node'));
expect(showMoreEl.length).toEqual(1);
expect(showMoreEl).toBeTruthy();
});
describe('when show more of top communities is clicked', () => {
beforeEach(fakeAsync(() => {
const showMoreLink = fixture.debugElement.query(By.css('.show-more-node a'));
showMoreLink.triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
tick();
fixture.detectChanges();
}));
it('tree contains maximum of currentPage (2) * (2) elementsPerPage of first top communities, or less if there are less communities (3)', () => {
const expandableNodesFound = fixture.debugElement.queryAll(By.css('.expandable-node a'));
const childlessNodesFound = fixture.debugElement.queryAll(By.css('.childless-node a'));
const allNodes = [...expandableNodesFound, ...childlessNodesFound];
expect(allNodes.length).toEqual(3);
mockTopFlatnodesUnexpanded.map((topFlatnode: FlatNode) => {
expect(allNodes.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === topFlatnode.name);
})).toBeTruthy();
});
});
it('show more node is gone from end of nodetree', () => {
const showMoreEl = fixture.debugElement.queryAll(By.css('.show-more-node'));
expect(showMoreEl.length).toEqual(0);
});
});
describe('when first expandable node is expanded', () => {
let allNodes;
beforeEach(fakeAsync(() => {
const chevronExpand = fixture.debugElement.query(By.css('.expandable-node button'));
const chevronExpandSpan = fixture.debugElement.query(By.css('.expandable-node button span'));
if (chevronExpandSpan.nativeElement.classList.contains('fa-chevron-right')) {
chevronExpand.nativeElement.click();
tick();
fixture.detectChanges();
}
const expandableNodesFound = fixture.debugElement.queryAll(By.css('.expandable-node a'));
const childlessNodesFound = fixture.debugElement.queryAll(By.css('.childless-node a'));
allNodes = [...expandableNodesFound, ...childlessNodesFound];
}));
describe('children of first expandable node are added to tree (page-limited)', () => {
it('tree contains page-limited topcoms (2) and children of first expandable node (2subcoms)', () => {
expect(allNodes.length).toEqual(4);
mockTopFlatnodesUnexpanded.slice(0, 2).map((topFlatnode: FlatNode) => {
expect(allNodes.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === topFlatnode.name);
})).toBeTruthy();
});
mockSubcommunities1Page1.map((subcom) => {
expect(allNodes.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === subcom.name);
})).toBeTruthy();
})
});
});
});
describe('second top community node is expanded and has more children (collections) than page size of collection', () => {
describe('children of second top com are added (page-limited pageSize 2)', () => {
let allNodes;
beforeEach(fakeAsync(() => {
const chevronExpand = fixture.debugElement.queryAll(By.css('.expandable-node button'));
const chevronExpandSpan = fixture.debugElement.queryAll(By.css('.expandable-node button span'));
if (chevronExpandSpan[1].nativeElement.classList.contains('fa-chevron-right')) {
chevronExpand[1].nativeElement.click();
tick();
fixture.detectChanges();
}
const expandableNodesFound = fixture.debugElement.queryAll(By.css('.expandable-node a'));
const childlessNodesFound = fixture.debugElement.queryAll(By.css('.childless-node a'));
allNodes = [...expandableNodesFound, ...childlessNodesFound];
}));
it('tree contains 2 (page-limited) top com, 2 (page-limited) coll of 2nd top com, a show more for those page-limited coll and show more for page-limited top com', () => {
mockTopFlatnodesUnexpanded.slice(0, 2).map((topFlatnode: FlatNode) => {
expect(allNodes.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === topFlatnode.name);
})).toBeTruthy();
});
mockCollectionsPage1.map((coll) => {
expect(allNodes.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === coll.name);
})).toBeTruthy();
});
expect(allNodes.length).toEqual(4);
const showMoreEl = fixture.debugElement.queryAll(By.css('.show-more-node'));
expect(showMoreEl.length).toEqual(2);
});
});
});
});

View File

@@ -0,0 +1,104 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { take } from 'rxjs/operators';
import { CommunityListService, FlatNode } from '../community-list-service';
import { CommunityListDatasource } from '../community-list-datasource';
import { FlatTreeControl } from '@angular/cdk/tree';
import { isEmpty } from '../../shared/empty.util';
/**
* A tree-structured list of nodes representing the communities, their subCommunities and collections.
* Initially only the page-restricted top communities are shown.
* Each node can be expanded to show its children and all children are also page-limited.
* More pages of a page-limited result can be shown by pressing a show more node/link.
* Which nodes were expanded is kept in the store, so this persists across pages.
*/
@Component({
selector: 'ds-community-list',
templateUrl: './community-list.component.html',
})
export class CommunityListComponent implements OnInit, OnDestroy {
private expandedNodes: FlatNode[] = [];
public loadingNode: FlatNode;
treeControl = new FlatTreeControl<FlatNode>(
(node) => node.level, (node) => true
);
dataSource: CommunityListDatasource;
constructor(private communityListService: CommunityListService) {
}
ngOnInit() {
this.dataSource = new CommunityListDatasource(this.communityListService);
this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => {
this.loadingNode = result;
});
this.communityListService.getExpandedNodesFromStore().pipe(take(1)).subscribe((result) => {
this.expandedNodes = [...result];
this.dataSource.loadCommunities(this.expandedNodes);
});
}
ngOnDestroy(): void {
this.communityListService.saveCommunityListStateToStore(this.expandedNodes, this.loadingNode);
}
// whether or not this node has children (subcommunities or collections)
hasChild(_: number, node: FlatNode) {
return node.isExpandable$;
}
// whether or not it is a show more node (contains no data, but is indication that there are more topcoms, subcoms or collections
isShowMore(_: number, node: FlatNode) {
return node.isShowMoreNode;
}
/**
* Toggles the expanded variable of a node, adds it to the exapanded nodes list and reloads the tree so this node is expanded
* @param node Node we want to expand
*/
toggleExpanded(node: FlatNode) {
this.loadingNode = node;
if (node.isExpanded) {
this.expandedNodes = this.expandedNodes.filter((node2) => node2.name !== node.name);
node.isExpanded = false;
} else {
this.expandedNodes.push(node);
node.isExpanded = true;
if (isEmpty(node.currentCollectionPage)) {
node.currentCollectionPage = 1;
}
if (isEmpty(node.currentCommunityPage)) {
node.currentCommunityPage = 1;
}
}
this.dataSource.loadCommunities(this.expandedNodes);
}
/**
* Makes sure the next page of a node is added to the tree (top community, sub community of collection)
* > Finds its parent (if not top community) and increases its corresponding collection/subcommunity currentPage
* > Reloads tree with new page added to corresponding top community lis, sub community list or collection list
* @param node The show more node indicating whether it's an increase in top communities, sub communities or collections
*/
getNextPage(node: FlatNode): void {
this.loadingNode = node;
if (node.parent != null) {
if (node.id === 'collection') {
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
parentNodeInExpandedNodes.currentCollectionPage++;
}
if (node.id === 'community') {
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
parentNodeInExpandedNodes.currentCommunityPage++;
}
this.dataSource.loadCommunities(this.expandedNodes);
} else {
this.communityListService.getNextPageTopCommunities();
this.dataSource.loadCommunities(this.expandedNodes);
}
}
}

View File

@@ -65,7 +65,7 @@ export class NormalizedItem extends NormalizedDSpaceObject<Item> {
@relationship(Bundle, true)
bundles: string[];
@autoserialize
@deserialize
@relationship(Relationship, true)
relationships: string[];

View File

@@ -1,6 +1,6 @@
/**
* Class representing a query parameter (query?fieldName=fieldValue) used in FindAllOptions object
* Class representing a query parameter (query?fieldName=fieldValue) used in FindListOptions object
*/
export class SearchParam {
constructor(public fieldName: string, public fieldValue: any) {

View File

@@ -3,7 +3,7 @@ import { TestScheduler } from 'rxjs/testing';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ConfigService } from './config.service';
import { RequestService } from '../data/request.service';
import { ConfigRequest, FindAllOptions } from '../data/request.models';
import { ConfigRequest, FindListOptions } from '../data/request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
@@ -27,7 +27,7 @@ describe('ConfigService', () => {
let requestService: RequestService;
let halService: any;
const findOptions: FindAllOptions = new FindAllOptions();
const findOptions: FindListOptions = new FindListOptions();
const scopeName = 'traditional';
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';

View File

@@ -2,7 +2,7 @@ import { merge as observableMerge, Observable, throwError as observableThrowErro
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
import { RequestService } from '../data/request.service';
import { ConfigSuccessResponse } from '../cache/response.models';
import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models';
import { ConfigRequest, FindListOptions, RestRequest } from '../data/request.models';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ConfigData } from './config-data';
@@ -35,7 +35,7 @@ export abstract class ConfigService {
return `${endpoint}/${resourceName}`;
}
protected getConfigSearchHref(endpoint, options: FindAllOptions = {}): string {
protected getConfigSearchHref(endpoint, options: FindListOptions = {}): string {
let result;
const args = [];
@@ -93,7 +93,7 @@ export abstract class ConfigService {
distinctUntilChanged());
}
public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> {
public getConfigBySearch(options: FindListOptions = {}): Observable<ConfigData> {
return this.halService.getEndpoint(this.linkPath).pipe(
map((endpoint: string) => this.getConfigSearchHref(endpoint, options)),
filter((href: string) => isNotEmpty(href)),

View File

@@ -11,7 +11,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { DeleteByIDRequest, FindAllOptions, PostRequest, PutRequest } from './request.models';
import { DeleteByIDRequest, FindListOptions, PostRequest, PutRequest } from './request.models';
import { Observable } from 'rxjs';
import { find, map, tap } from 'rxjs/operators';
import { configureRequest, getResponseFromEntry } from '../shared/operators';
@@ -54,10 +54,10 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
/**
* Get the endpoint for browsing bitstream formats
* @param {FindAllOptions} options
* @param {FindListOptions} options
* @returns {Observable<string>}
*/
getBrowseEndpoint(options: FindAllOptions = {}, linkPath?: string): Observable<string> {
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}

View File

@@ -11,7 +11,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { FindAllOptions } from './request.models';
import { FindListOptions } from './request.models';
import { Observable } from 'rxjs/internal/Observable';
/**
@@ -37,10 +37,10 @@ export class BundleDataService extends DataService<Bundle> {
/**
* Get the endpoint for browsing bundles
* @param {FindAllOptions} options
* @param {FindListOptions} options
* @returns {Observable<string>}
*/
getBrowseEndpoint(options: FindAllOptions = {}, linkPath?: string): Observable<string> {
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { distinctUntilChanged, filter, map, take } from 'rxjs/operators';
import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -16,7 +16,7 @@ import { HttpClient } from '@angular/common/http';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { Observable } from 'rxjs/internal/Observable';
import { FindAllOptions, GetRequest } from './request.models';
import {FindListOptions, FindListRequest, GetRequest} from './request.models';
import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list';
import { configureRequest } from '../shared/operators';
@@ -50,11 +50,11 @@ export class CollectionDataService extends ComColDataService<Collection> {
/**
* Get all collections the user is authorized to submit to
*
* @param options The [[FindAllOptions]] object
* @param options The [[FindListOptions]] object
* @return Observable<RemoteData<PaginatedList<Collection>>>
* collection list
*/
getAuthorizedCollection(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
getAuthorizedCollection(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findAuthorized';
return this.searchBy(searchHref, options).pipe(
@@ -65,11 +65,11 @@ export class CollectionDataService extends ComColDataService<Collection> {
* Get all collections the user is authorized to submit to, by community
*
* @param communityId The community id
* @param options The [[FindAllOptions]] object
* @param options The [[FindListOptions]] object
* @return Observable<RemoteData<PaginatedList<Collection>>>
* collection list
*/
getAuthorizedCollectionByCommunity(communityId: string, options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findAuthorizedByCommunity';
options.searchParams = [new SearchParam('uuid', communityId)];
@@ -85,7 +85,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
*/
hasAuthorizedCollection(): Observable<boolean> {
const searchHref = 'findAuthorized';
const options = new FindAllOptions();
const options = new FindListOptions();
options.elementsPerPage = 1;
return this.searchBy(searchHref, options).pipe(
@@ -136,4 +136,10 @@ export class CollectionDataService extends ComColDataService<Collection> {
return this.rdbService.buildList(href$);
}
protected getFindByParentHref(parentUUID: string): Observable<string> {
return this.halService.getEndpoint('communities').pipe(
switchMap((communityEndpointHref: string) =>
this.halService.getEndpoint('collections', `${communityEndpointHref}/${parentUUID}`)),
);
}
}

View File

@@ -8,12 +8,12 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers';
import { ComColDataService } from './comcol-data.service';
import { CommunityDataService } from './community-data.service';
import { FindAllOptions, FindByIDRequest } from './request.models';
import { FindListOptions, FindByIDRequest } from './request.models';
import { RequestService } from './request.service';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestEntry } from './request.reducer';
import { of as observableOf } from 'rxjs';
import {Observable, of as observableOf} from 'rxjs';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
@@ -45,6 +45,11 @@ class TestService extends ComColDataService<any> {
) {
super();
}
protected getFindByParentHref(parentUUID: string): Observable<string> {
// implementation in subclasses for communities/collections
return undefined;
}
}
/* tslint:enable:max-classes-per-file */
@@ -66,7 +71,7 @@ describe('ComColDataService', () => {
const dataBuildService = {} as NormalizedObjectBuildService;
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
const options = Object.assign(new FindAllOptions(), {
const options = Object.assign(new FindListOptions(), {
scopeID: scopeID
});
const getRequestEntry$ = (successful: boolean) => {

View File

@@ -1,12 +1,23 @@
import { distinctUntilChanged, filter, map, mergeMap, share, take, tap } from 'rxjs/operators';
import {
distinctUntilChanged,
filter, first,
map,
mergeMap,
share,
switchMap,
take,
tap
} from 'rxjs/operators';
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { CommunityDataService } from './community-data.service';
import { DataService } from './data.service';
import { FindAllOptions, FindByIDRequest } from './request.models';
import { PaginatedList } from './paginated-list';
import { RemoteData } from './remote-data';
import { FindListOptions, FindByIDRequest } from './request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { getResponseFromEntry } from '../shared/operators';
import { CacheableObject } from '../cache/object-cache.reducer';
@@ -26,7 +37,7 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS
* @return { Observable<string> }
* an Observable<string> containing the scoped URL
*/
public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
if (isEmpty(options.scopeID)) {
return this.halService.getEndpoint(linkPath);
} else {
@@ -57,4 +68,12 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS
return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged(), share());
}
}
protected abstract getFindByParentHref(parentUUID: string): Observable<string>;
public findByParent(parentUUID: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<T>>> {
const href$ = this.buildHrefFromFindOptions(this.getFindByParentHref(parentUUID), [], options);
return this.findList(href$, options);
}
}

View File

@@ -1,4 +1,4 @@
import { filter, take } from 'rxjs/operators';
import { filter, switchMap, take } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
@@ -9,7 +9,7 @@ import { Community } from '../shared/community.model';
import { ComColDataService } from './comcol-data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions, FindAllRequest } from './request.models';
import { FindListOptions, FindListRequest } from './request.models';
import { RemoteData } from './remote-data';
import { hasValue } from '../../shared/empty.util';
import { Observable } from 'rxjs';
@@ -43,16 +43,24 @@ export class CommunityDataService extends ComColDataService<Community> {
return this.halService.getEndpoint(this.linkPath);
}
findTop(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Community>>> {
findTop(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Community>>> {
const hrefObs = this.getFindAllHref(options, this.topLinkPath);
hrefObs.pipe(
filter((href: string) => hasValue(href)),
take(1))
.subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request);
});
return this.rdbService.buildList<Community>(hrefObs) as Observable<RemoteData<PaginatedList<Community>>>;
}
protected getFindByParentHref(parentUUID: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
switchMap((communityEndpointHref: string) =>
this.halService.getEndpoint('subcommunities', `${communityEndpointHref}/${parentUUID}`))
);
}
}

View File

@@ -6,7 +6,7 @@ import { CoreState } from '../core.reducers';
import { Store } from '@ngrx/store';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Observable, of as observableOf } from 'rxjs';
import { FindAllOptions } from './request.models';
import { FindListOptions } from './request.models';
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { compare, Operation } from 'fast-json-patch';
@@ -40,7 +40,7 @@ class TestService extends DataService<any> {
super();
}
public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return observableOf(endpoint);
}
}
@@ -53,7 +53,7 @@ class DummyChangeAnalyzer implements ChangeAnalyzer<NormalizedTestObject> {
}
describe('DataService', () => {
let service: TestService;
let options: FindAllOptions;
let options: FindListOptions;
const requestService = {} as RequestService;
const halService = {} as HALEndpointService;
const rdbService = {} as RemoteDataBuildService;

View File

@@ -14,8 +14,8 @@ import { RemoteData } from './remote-data';
import {
CreateRequest,
DeleteByIDRequest,
FindAllOptions,
FindAllRequest,
FindListOptions,
FindListRequest,
FindByIDRequest,
GetRequest
} from './request.models';
@@ -54,17 +54,17 @@ export abstract class DataService<T extends CacheableObject> {
*/
protected responseMsToLive: number;
public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
public abstract getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable<string>
/**
* Create the HREF with given options object
*
* @param options The [[FindAllOptions]] object
* @param options The [[FindListOptions]] object
* @param linkPath The link path for the object
* @return {Observable<string>}
* Return an observable that emits created HREF
*/
protected getFindAllHref(options: FindAllOptions = {}, linkPath?: string): Observable<string> {
protected getFindAllHref(options: FindListOptions = {}, linkPath?: string): Observable<string> {
let result: Observable<string>;
const args = [];
@@ -77,11 +77,11 @@ export abstract class DataService<T extends CacheableObject> {
* Create the HREF for a specific object's search method with given options object
*
* @param searchMethod The search method for the object
* @param options The [[FindAllOptions]] object
* @param options The [[FindListOptions]] object
* @return {Observable<string>}
* Return an observable that emits created HREF
*/
protected getSearchByHref(searchMethod: string, options: FindAllOptions = {}): Observable<string> {
protected getSearchByHref(searchMethod: string, options: FindListOptions = {}): Observable<string> {
let result: Observable<string>;
const args = [];
@@ -101,11 +101,11 @@ export abstract class DataService<T extends CacheableObject> {
*
* @param href$ The HREF to which the query string should be appended
* @param args Array with additional params to combine with query string
* @param options The [[FindAllOptions]] object
* @param options The [[FindListOptions]] object
* @return {Observable<string>}
* Return an observable that emits created HREF
*/
protected buildHrefFromFindOptions(href$: Observable<string>, args: string[], options: FindAllOptions): Observable<string> {
protected buildHrefFromFindOptions(href$: Observable<string>, args: string[], options: FindListOptions): Observable<string> {
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
@@ -127,20 +127,22 @@ export abstract class DataService<T extends CacheableObject> {
}
}
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<T>>> {
const hrefObs = this.getFindAllHref(options);
findAll(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<T>>> {
return this.findList(this.getFindAllHref(options), options);
}
hrefObs.pipe(
protected findList(href$, options: FindListOptions) {
href$.pipe(
first((href: string) => hasValue(href)))
.subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.configure(request);
});
return this.rdbService.buildList<T>(hrefObs) as Observable<RemoteData<PaginatedList<T>>>;
return this.rdbService.buildList<T>(href$) as Observable<RemoteData<PaginatedList<T>>>;
}
/**
@@ -191,21 +193,21 @@ export abstract class DataService<T extends CacheableObject> {
}
/**
* Make a new FindAllRequest with given search method
* Make a new FindListRequest with given search method
*
* @param searchMethod The search method for the object
* @param options The [[FindAllOptions]] object
* @param options The [[FindListOptions]] object
* @return {Observable<RemoteData<PaginatedList<T>>}
* Return an observable that emits response from the server
*/
protected searchBy(searchMethod: string, options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<T>>> {
protected searchBy(searchMethod: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<T>>> {
const hrefObs = this.getSearchByHref(searchMethod, options);
hrefObs.pipe(
first((href: string) => hasValue(href)))
.subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
request.responseMsToLive = 10 * 1000;
this.requestService.configure(request);
});

View File

@@ -8,7 +8,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
import { RequestService } from './request.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { FindAllOptions, FindByIDRequest, IdentifierType } from './request.models';
import { FindListOptions, FindByIDRequest, IdentifierType } from './request.models';
import { Observable } from 'rxjs';
import { RemoteData } from './remote-data';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
@@ -40,7 +40,7 @@ export class DsoRedirectDataService extends DataService<any> {
super();
}
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return this.halService.getEndpoint(linkPath);
}

View File

@@ -8,7 +8,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DataService } from './data.service';
import { RemoteData } from './remote-data';
import { RequestService } from './request.service';
import { FindAllOptions } from './request.models';
import { FindListOptions } from './request.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
@@ -32,7 +32,7 @@ class DataServiceImpl extends DataService<DSpaceObject> {
super();
}
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return this.halService.getEndpoint(linkPath);
}

View File

@@ -2,14 +2,13 @@ import { Store } from '@ngrx/store';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { CoreState } from '../core.reducers';
import { ItemDataService } from './item-data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import {
DeleteRequest,
FindAllOptions,
FindListOptions,
GetRequest,
MappedCollectionsRequest,
PostRequest,
@@ -58,7 +57,7 @@ describe('ItemDataService', () => {
} as HALEndpointService;
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
const options = Object.assign(new FindAllOptions(), {
const options = Object.assign(new FindListOptions(), {
scopeID: scopeID,
sort: {
field: '',

View File

@@ -14,7 +14,7 @@ import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import {
DeleteRequest,
FindAllOptions,
FindListOptions,
MappedCollectionsRequest,
PatchRequest,
PostRequest, PutRequest,
@@ -59,10 +59,10 @@ export class ItemDataService extends DataService<Item> {
/**
* Get the endpoint for browsing items
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
* @param {FindAllOptions} options
* @param {FindListOptions} options
* @returns {Observable<string>}
*/
public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
let field = 'dc.date.issued';
if (options.sort && options.sort.field) {
field = options.sort.field;
@@ -247,4 +247,14 @@ export class ItemDataService extends DataService<Item> {
map((request: RequestEntry) => request.response)
);
}
/**
* Get the endpoint for an item's bitstreams
* @param itemId
*/
public getBitstreamsEndpoint(itemId: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`))
);
}
}

View File

@@ -7,7 +7,7 @@ import { CoreState } from '../core.reducers';
import { DataService } from './data.service';
import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions } from './request.models';
import { FindListOptions } from './request.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { HttpClient } from '@angular/common/http';
@@ -33,7 +33,7 @@ class DataServiceImpl extends DataService<MetadataSchema> {
super();
}
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return this.halService.getEndpoint(linkPath);
}
}

View File

@@ -10,7 +10,7 @@ import {
getRemoteDataPayload, getResponseFromEntry,
getSucceededRemoteData
} from '../shared/operators';
import { DeleteRequest, FindAllOptions, RestRequest } from './request.models';
import { DeleteRequest, FindListOptions, RestRequest } from './request.models';
import { Observable } from 'rxjs/internal/Observable';
import { RestResponse } from '../cache/response.models';
import { Item } from '../shared/item.model';
@@ -56,7 +56,7 @@ export class RelationshipService extends DataService<Relationship> {
super();
}
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return this.halService.getEndpoint(linkPath);
}
@@ -228,7 +228,7 @@ export class RelationshipService extends DataService<Relationship> {
* @param label
* @param options
*/
getRelatedItemsByLabel(item: Item, label: string, options?: FindAllOptions): Observable<RemoteData<PaginatedList<Item>>> {
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
return this.getItemRelationshipsByLabel(item, label, options).pipe(paginatedRelationsToItems(item.uuid));
}
@@ -239,18 +239,18 @@ export class RelationshipService extends DataService<Relationship> {
* @param label
* @param options
*/
getItemRelationshipsByLabel(item: Item, label: string, options?: FindAllOptions): Observable<RemoteData<PaginatedList<Relationship>>> {
let findAllOptions = new FindAllOptions();
getItemRelationshipsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Relationship>>> {
let findListOptions = new FindListOptions();
if (options) {
findAllOptions = Object.assign(new FindAllOptions(), options);
findListOptions = Object.assign(new FindListOptions(), options);
}
const searchParams = [ new SearchParam('label', label), new SearchParam('dso', item.id) ];
if (findAllOptions.searchParams) {
findAllOptions.searchParams = [...findAllOptions.searchParams, ...searchParams];
if (findListOptions.searchParams) {
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
} else {
findAllOptions.searchParams = searchParams;
findListOptions.searchParams = searchParams;
}
return this.searchBy('byLabel', findAllOptions);
return this.searchBy('byLabel', findListOptions);
}
/**

View File

@@ -138,7 +138,7 @@ export class FindByIDRequest extends GetRequest {
}
}
export class FindAllOptions {
export class FindListOptions {
scopeID?: string;
elementsPerPage?: number;
currentPage?: number;
@@ -147,11 +147,11 @@ export class FindAllOptions {
startsWith?: string;
}
export class FindAllRequest extends GetRequest {
export class FindListRequest extends GetRequest {
constructor(
uuid: string,
href: string,
public body?: FindAllOptions,
public body?: FindListOptions,
) {
super(uuid, href);
}

View File

@@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { FindAllOptions } from '../data/request.models';
import { FindListOptions } from '../data/request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ResourcePolicy } from '../shared/resource-policy.model';
import { RemoteData } from '../data/remote-data';
@@ -36,7 +36,7 @@ class DataServiceImpl extends DataService<ResourcePolicy> {
super();
}
getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return this.halService.getEndpoint(linkPath);
}
}

View File

@@ -13,7 +13,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { of as observableOf } from 'rxjs';
import { RestResponse } from '../cache/response.models';
import { RequestEntry } from './request.reducer';
import { FindAllOptions } from './request.models';
import { FindListOptions } from './request.models';
import { TestScheduler } from 'rxjs/testing';
import { PaginatedList } from './paginated-list';
import { RemoteData } from './remote-data';
@@ -31,7 +31,7 @@ describe('SiteDataService', () => {
});
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
const options = Object.assign(new FindAllOptions(), {});
const options = Object.assign(new FindListOptions(), {});
const getRequestEntry$ = (successful:boolean, statusCode:number, statusText:string) => {
return observableOf({

View File

@@ -10,7 +10,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { FindAllOptions } from './request.models';
import { FindListOptions } from './request.models';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { RemoteData } from './remote-data';
@@ -46,10 +46,10 @@ export class SiteDataService extends DataService<Site> {
/**
* Get the endpoint for browsing the site object
* @param {FindAllOptions} options
* @param {FindListOptions} options
* @param {Observable<string>} linkPath
*/
getBrowseEndpoint(options:FindAllOptions, linkPath?:string):Observable<string> {
getBrowseEndpoint(options:FindListOptions, linkPath?:string):Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}

View File

@@ -1,5 +1,5 @@
import { Observable } from 'rxjs';
import { FindAllOptions } from '../data/request.models';
import { FindListOptions } from '../data/request.models';
import { DataService } from '../data/data.service';
import { CacheableObject } from '../cache/object-cache.reducer';
@@ -8,7 +8,7 @@ import { CacheableObject } from '../cache/object-cache.reducer';
*/
export abstract class EpersonService<TDomain extends CacheableObject> extends DataService<TDomain> {
public getBrowseEndpoint(options: FindAllOptions): Observable<string> {
public getBrowseEndpoint(options: FindListOptions): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
}

View File

@@ -7,7 +7,7 @@ import { filter, map, take } from 'rxjs/operators';
import { EpersonService } from './eperson.service';
import { RequestService } from '../data/request.service';
import { FindAllOptions } from '../data/request.models';
import { FindListOptions } from '../data/request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Group } from './models/group.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -52,7 +52,7 @@ export class GroupEpersonService extends EpersonService<Group> {
*/
isMemberOf(groupName: string): Observable<boolean> {
const searchHref = 'isMemberOf';
const options = new FindAllOptions();
const options = new FindListOptions();
options.searchParams = [new SearchParam('groupName', groupName)];
return this.searchBy(searchHref, options).pipe(

View File

@@ -43,8 +43,8 @@ export class HALEndpointService {
);
}
public getEndpoint(linkPath: string): Observable<string> {
return this.getEndpointAt(this.getRootHref(), ...linkPath.split('/'));
public getEndpoint(linkPath: string, startHref?: string): Observable<string> {
return this.getEndpointAt(startHref || this.getRootHref(), ...linkPath.split('/'));
}
/**

View File

@@ -8,7 +8,7 @@ import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { WorkflowItem } from './models/workflowitem.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions } from '../data/request.models';
import { FindListOptions } from '../data/request.models';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { ObjectCacheService } from '../cache/object-cache.service';
@@ -35,7 +35,7 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
super();
}
public getBrowseEndpoint(options: FindAllOptions) {
public getBrowseEndpoint(options: FindListOptions) {
return this.halService.getEndpoint(this.linkPath);
}

View File

@@ -7,7 +7,7 @@ import { CoreState } from '../core.reducers';
import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions } from '../data/request.models';
import { FindListOptions } from '../data/request.models';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { ObjectCacheService } from '../cache/object-cache.service';
@@ -35,7 +35,7 @@ export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
super();
}
public getBrowseEndpoint(options: FindAllOptions) {
public getBrowseEndpoint(options: FindListOptions) {
return this.halService.getEndpoint(this.linkPath);
}

View File

@@ -4,7 +4,7 @@ import { merge as observableMerge, Observable, of as observableOf } from 'rxjs';
import { distinctUntilChanged, filter, flatMap, map, mergeMap, tap } from 'rxjs/operators';
import { DataService } from '../data/data.service';
import { DeleteRequest, FindAllOptions, PostRequest, TaskDeleteRequest, TaskPostRequest } from '../data/request.models';
import { DeleteRequest, FindListOptions, PostRequest, TaskDeleteRequest, TaskPostRequest } from '../data/request.models';
import { isNotEmpty } from '../../shared/empty.util';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { ProcessTaskResponse } from './models/process-task-response';
@@ -18,7 +18,7 @@ import { CacheableObject } from '../cache/object-cache.reducer';
*/
export abstract class TasksService<T extends CacheableObject> extends DataService<T> {
public getBrowseEndpoint(options: FindAllOptions): Observable<string> {
public getBrowseEndpoint(options: FindListOptions): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}

View File

@@ -36,8 +36,11 @@
</div>
</div>
<div class="mt-5 w-100">
<ds-related-entities-search [item]="object"
[relationType]="'isJournalOfPublication'">
</ds-related-entities-search>
<ds-tabbed-related-entities-search [item]="object"
[relationTypes]="[{
label: 'isJournalOfPublication',
filter: 'isJournalOfPublication'
}]">
</ds-tabbed-related-entities-search>
</div>
</div>

View File

@@ -24,16 +24,6 @@
</ds-generic-item-page-field>
</div>
<div class="col-xs-12 col-md-6">
<ds-related-items
[parentItem]="object"
[relationType]="'isPersonOfOrgUnit'"
[label]="'relationships.isPersonOf' | translate">
</ds-related-items>
<ds-related-items
[parentItem]="object"
[relationType]="'isProjectOfOrgUnit'"
[label]="'relationships.isProjectOf' | translate">
</ds-related-items>
<ds-related-items
[parentItem]="object"
[relationType]="'isPublicationOfOrgUnit'"
@@ -49,4 +39,18 @@
</a>
</div>
</div>
<div class="mt-5 w-100">
<ds-tabbed-related-entities-search [item]="object"
[relationTypes]="[{
label: 'isOrgUnitOfPerson',
filter: 'isOrgUnitOfPerson',
configuration: 'person'
},
{
label: 'isOrgUnitOfProject',
filter: 'isOrgUnitOfProject',
configuration: 'project'
}]">
</ds-tabbed-related-entities-search>
</div>
</div>

View File

@@ -53,8 +53,11 @@
</div>
</div>
<div class="mt-5 w-100">
<ds-related-entities-search [item]="object"
[relationType]="'isAuthorOfPublication'">
</ds-related-entities-search>
<ds-tabbed-related-entities-search [item]="object"
[relationTypes]="[{
label: 'isAuthorOfPublication',
filter: 'isAuthorOfPublication'
}]">
</ds-tabbed-related-entities-search>
</div>
</div>

View File

@@ -53,17 +53,18 @@ export class NavbarComponent extends MenuComponent implements OnInit {
} as TextMenuItemModel,
index: 0
},
// {
// id: 'browse_global_communities_and_collections',
// parentID: 'browse_global',
// active: false,
// visible: true,
// model: {
// type: MenuItemType.LINK,
// text: 'menu.section.browse_global_communities_and_collections',
// link: '#'
// } as LinkMenuItemModel,
// },
/* Communities & Collections tree */
{
id: `browse_global_communities_and_collections`,
parentID: 'browse_global',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: `menu.section.browse_global_communities_and_collections`,
link: `/community-list`
} as LinkMenuItemModel
},
/* Statistics */
{

View File

@@ -36,7 +36,7 @@ import { SubmissionService } from '../../submission.service';
import { SubmissionObject } from '../../../core/submission/models/submission-object.model';
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
import { CollectionDataService } from '../../../core/data/collection-data.service';
import { FindAllOptions } from '../../../core/data/request.models';
import { FindListOptions } from '../../../core/data/request.models';
/**
* An interface to represent a collection entry
@@ -205,7 +205,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
map((collectionRD: RemoteData<Collection>) => collectionRD.payload.name)
);
const findOptions: FindAllOptions = {
const findOptions: FindListOptions = {
elementsPerPage: 1000
};

View File

@@ -61,7 +61,10 @@
<div class="container search-container">
<h3 class="h2">{{"item.page.journal.search.title" | translate}}</h3>
</div>
<ds-related-entities-search [item]="object"
[relationType]="'isJournalOfPublication'">
</ds-related-entities-search>
<ds-tabbed-related-entities-search [item]="object"
[relationTypes]="[{
label: 'isJournalOfPublication',
filter: 'isJournalOfPublication'
}]">
</ds-tabbed-related-entities-search>
</div>

View File

@@ -53,18 +53,6 @@
<div class="relationships-item-page">
<div class="container">
<div class="row">
<ds-related-items
class="col-12 col-md-4"
[parentItem]="object"
[relationType]="'isPersonOfOrgUnit'"
[label]="'relationships.isPersonOf' | translate">
</ds-related-items>
<ds-related-items
class="col-12 col-md-4"
[parentItem]="object"
[relationType]="'isProjectOfOrgUnit'"
[label]="'relationships.isProjectOf' | translate">
</ds-related-items>
<ds-related-items
class="col-12 col-md-4"
[parentItem]="object"
@@ -74,3 +62,20 @@
</div>
</div>
</div>
<div class="container">
<div class="row">
<ds-tabbed-related-entities-search class="w-100"
[item]="object"
[relationTypes]="[{
label: 'isOrgUnitOfPerson',
filter: 'isOrgUnitOfPerson',
configuration: 'person'
},
{
label: 'isOrgUnitOfProject',
filter: 'isOrgUnitOfProject',
configuration: 'project'
}]">
</ds-tabbed-related-entities-search>
</div>
</div>

View File

@@ -1,4 +1,4 @@
@import 'src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.scss';
@import 'src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss';
:host {
> * {

View File

@@ -79,7 +79,10 @@
<div class="container search-container">
<h3 class="h2">{{"item.page.person.search.title" | translate}}</h3>
</div>
<ds-related-entities-search [item]="object"
[relationType]="'isAuthorOfPublication'">
</ds-related-entities-search>
<ds-tabbed-related-entities-search [item]="object"
[relationTypes]="[{
label: 'isAuthorOfPublication',
filter: 'isAuthorOfPublication'
}]">
</ds-tabbed-related-entities-search>
</div>

View File

@@ -35,6 +35,13 @@
dependencies:
tslib "^1.9.0"
"@angular/cdk@^6.4.7":
version "6.4.7"
resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-6.4.7.tgz#1549b304dd412e82bd854cc55a7d5c6772ee0411"
integrity sha512-18x0U66fLD5kGQWZ9n3nb75xQouXlWs7kUDaTd8HTrHpT1s2QIAqlLd1KxfrYiVhsEC2jPQaoiae7VnBlcvkBg==
dependencies:
tslib "^1.7.1"
"@angular/cli@^6.1.5":
version "6.1.5"
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-6.1.5.tgz#312c062631285ff06fd07ecde8afe22cdef5a0e1"
@@ -10809,6 +10816,11 @@ tsickle@^0.32.1:
source-map "^0.6.0"
source-map-support "^0.5.0"
tslib@^1.7.1:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"