mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'dspace/master' into w2p-65240_Community-and-collection-logos-2
# Conflicts: # src/app/+collection-page/collection-page.component.html # src/app/core/data/comcol-data.service.ts
This commit is contained in:
@@ -2,7 +2,8 @@ import { browser, element, by } from 'protractor';
|
||||
|
||||
export class ProtractorPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
return browser.get('/')
|
||||
.then(() => browser.waitForAngular());
|
||||
}
|
||||
|
||||
getPageTitleText() {
|
||||
|
@@ -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",
|
||||
@@ -228,7 +229,7 @@
|
||||
"rollup-plugin-node-globals": "1.2.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.3",
|
||||
"rollup-plugin-terser": "^2.0.2",
|
||||
"sass-loader": "7.1.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"script-ext-html-webpack-plugin": "2.0.1",
|
||||
"source-map": "0.7.3",
|
||||
"source-map-loader": "0.2.4",
|
||||
|
3
resources/fonts/README.md
Normal file
3
resources/fonts/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Supported font formats
|
||||
|
||||
DSpace supports EOT, TTF, OTF, SVG, WOFF and WOFF2 fonts.
|
@@ -382,6 +382,14 @@
|
||||
|
||||
|
||||
|
||||
"communityList.tabTitle": "DSpace - Community List",
|
||||
|
||||
"communityList.title": "List of Communities",
|
||||
|
||||
"communityList.showMore": "Show More",
|
||||
|
||||
|
||||
|
||||
"community.create.head": "Create a Community",
|
||||
|
||||
"community.create.notifications.success": "Successfully created the Community",
|
||||
@@ -900,6 +908,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",
|
||||
@@ -1350,6 +1366,8 @@
|
||||
|
||||
"project.page.titleprefix": "Research Project: ",
|
||||
|
||||
"project.search.results.head": "Project Search Results",
|
||||
|
||||
|
||||
|
||||
"publication.listelement.badge": "Publication",
|
||||
|
@@ -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;
|
||||
|
@@ -3,7 +3,13 @@
|
||||
*ngVar="(collectionRD$ | async) as collectionRD">
|
||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="collectionRD?.payload as collection">
|
||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||
<ds-view-tracker [object]="collection"></ds-view-tracker>
|
||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||
<!-- Collection logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||
[logo]="(logoRD$ | async)?.payload" [alternateText]="'Collection Logo'">
|
||||
[alternateText]="'Collection Logo'">
|
||||
</ds-comcol-page-logo>
|
||||
<!-- Collection Name -->
|
||||
<ds-comcol-page-header
|
||||
[name]="collection.name">
|
||||
|
@@ -11,12 +11,14 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CollectionPageRoutingModule
|
||||
CollectionPageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(communityRD$ | async) as communityRD">
|
||||
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="communityRD?.payload; let communityPayload">
|
||||
<ds-view-tracker [object]="communityPayload"></ds-view-tracker>
|
||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||
|
||||
<!-- Community name -->
|
||||
|
@@ -6,16 +6,18 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
||||
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
||||
import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component';
|
||||
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||
import { CommunityFormComponent } from './community-form/community-form.component';
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CommunityPageRoutingModule
|
||||
CommunityPageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
CommunityPageComponent,
|
||||
|
@@ -2,12 +2,25 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { HomePageComponent } from './home-page.component';
|
||||
import { HomePageResolver } from './home-page.resolver';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: '', component: HomePageComponent, pathMatch: 'full', data: { title: 'home.title' } }
|
||||
{
|
||||
path: '',
|
||||
component: HomePageComponent,
|
||||
pathMatch: 'full',
|
||||
data: {title: 'home.title'},
|
||||
resolve: {
|
||||
site: HomePageResolver
|
||||
}
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
HomePageResolver
|
||||
]
|
||||
})
|
||||
export class HomePageRoutingModule { }
|
||||
export class HomePageRoutingModule {
|
||||
}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
<ds-home-news></ds-home-news>
|
||||
<div class="container">
|
||||
<ng-container *ngIf="(site$ | async) as site">
|
||||
<ds-view-tracker [object]="site"></ds-view-tracker>
|
||||
</ng-container>
|
||||
<ds-search-form [inPlaceSearch]="false"></ds-search-form>
|
||||
<ds-top-level-community-list></ds-top-level-community-list>
|
||||
</div>
|
||||
|
@@ -1,9 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Site } from '../core/shared/site.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-home-page',
|
||||
styleUrls: ['./home-page.component.scss'],
|
||||
templateUrl: './home-page.component.html'
|
||||
})
|
||||
export class HomePageComponent {
|
||||
export class HomePageComponent implements OnInit {
|
||||
|
||||
site$:Observable<Site>;
|
||||
|
||||
constructor(
|
||||
private route:ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
this.site$ = this.route.data.pipe(
|
||||
map((data) => data.site as Site),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -6,12 +6,14 @@ import { HomePageRoutingModule } from './home-page-routing.module';
|
||||
|
||||
import { HomePageComponent } from './home-page.component';
|
||||
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
HomePageRoutingModule
|
||||
HomePageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
HomePageComponent,
|
||||
|
25
src/app/+home-page/home-page.resolver.ts
Normal file
25
src/app/+home-page/home-page.resolver.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { SiteDataService } from '../core/data/site-data.service';
|
||||
import { Site } from '../core/shared/site.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* The class that resolve the Site object for a route
|
||||
*/
|
||||
@Injectable()
|
||||
export class HomePageResolver implements Resolve<Site> {
|
||||
constructor(private siteService:SiteDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving a site object
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<Site> Emits the found Site object, or an error if something went wrong
|
||||
*/
|
||||
resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<Site> | Promise<Site> | Site {
|
||||
return this.siteService.find().pipe(take(1));
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||
<div class="simple-view-link my-3">
|
||||
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id]">
|
||||
|
@@ -26,6 +26,8 @@ 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({
|
||||
imports: [
|
||||
@@ -33,7 +35,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
|
||||
SharedModule,
|
||||
ItemPageRoutingModule,
|
||||
EditItemPageModule,
|
||||
SearchPageModule
|
||||
SearchPageModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
ItemPageComponent,
|
||||
@@ -53,7 +56,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
|
||||
ItemComponent,
|
||||
GenericItemPageFieldComponent,
|
||||
MetadataRepresentationListComponent,
|
||||
RelatedEntitiesSearchComponent
|
||||
RelatedEntitiesSearchComponent,
|
||||
TabbedRelatedEntitiesSearchComponent
|
||||
],
|
||||
exports: [
|
||||
ItemComponent,
|
||||
@@ -63,7 +67,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
|
||||
RelatedEntitiesSearchComponent,
|
||||
RelatedItemsComponent,
|
||||
MetadataRepresentationListComponent,
|
||||
ItemPageTitleFieldComponent
|
||||
ItemPageTitleFieldComponent,
|
||||
TabbedRelatedEntitiesSearchComponent
|
||||
],
|
||||
entryComponents: [
|
||||
PublicationComponent
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
})
|
||||
});
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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>
|
@@ -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'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
||||
|
@@ -1 +1 @@
|
||||
@import '../+search-page/search-page.component.scss';
|
||||
@import '../+search-page/search.component.scss';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { configureSearchComponentTestingModule } from './search-page.component.spec';
|
||||
import { configureSearchComponentTestingModule } from './search.component.spec';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
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';
|
||||
@@ -16,8 +16,8 @@ import { RouteService } from '../core/services/route.service';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-configuration-search-page',
|
||||
styleUrls: ['./search-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
@@ -28,13 +28,19 @@ import { RouteService } from '../core/services/route.service';
|
||||
]
|
||||
})
|
||||
|
||||
export class ConfigurationSearchPageComponent extends SearchPageComponent implements OnInit {
|
||||
export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit {
|
||||
/**
|
||||
* The configuration to use for the search options
|
||||
* If empty, the configuration will be determined by the route parameter called 'configuration'
|
||||
*/
|
||||
@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 SearchPageComponent implem
|
||||
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
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -1,21 +0,0 @@
|
||||
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { configureSearchComponentTestingModule } from './search-page.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();
|
||||
});
|
||||
});
|
@@ -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 { SearchPageComponent } from './search-page.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-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export class FilteredSearchPageComponent extends SearchPageComponent 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 });
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@@ -1,50 +1,2 @@
|
||||
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
|
||||
<div class="row">
|
||||
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async"
|
||||
[configuration]="configuration$ | async"
|
||||
[disableHeader]="!searchEnabled"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</ds-page-with-sidebar>
|
||||
|
||||
<ng-template #sidebarContent>
|
||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
>
|
||||
</ds-search-sidebar>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #searchForm>
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
</ng-template>
|
||||
<ds-search></ds-search>
|
||||
<ds-search-tracker></ds-search-tracker>
|
||||
|
@@ -1,184 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { startWith, switchMap, } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { SearchResult } from './search-result.model';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
|
||||
export const SEARCH_ROUTE = '/search';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-page',
|
||||
styleUrls: ['./search-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* This component represents the whole search page
|
||||
* It renders search results depending on the current search options
|
||||
*/
|
||||
export class SearchPageComponent implements OnInit {
|
||||
/**
|
||||
* The current search results
|
||||
*/
|
||||
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* The current paginated search options
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
*/
|
||||
scopeListRD$: Observable<DSpaceObject[]>;
|
||||
|
||||
/**
|
||||
* Emits true if were on a small screen
|
||||
*/
|
||||
isXsOrSm$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
/**
|
||||
* True when the search component should show results on the current page
|
||||
*/
|
||||
@Input() inPlaceSearch = true;
|
||||
|
||||
/**
|
||||
* Whether or not the search bar should be visible
|
||||
*/
|
||||
@Input()
|
||||
searchEnabled = true;
|
||||
|
||||
/**
|
||||
* The width of the sidebar (bootstrap columns)
|
||||
*/
|
||||
@Input()
|
||||
sideBarWidth = 3;
|
||||
|
||||
/**
|
||||
* The currently applied configuration (determines title of search)
|
||||
*/
|
||||
@Input()
|
||||
configuration$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Link to the search page
|
||||
*/
|
||||
searchLink: string;
|
||||
|
||||
/**
|
||||
* Observable for whether or not the sidebar is currently collapsed
|
||||
*/
|
||||
isSidebarCollapsed$: Observable<boolean>;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
|
||||
this.searchLink = this.getSearchLink();
|
||||
this.searchOptions$ = this.getSearchOptions();
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined))))
|
||||
.subscribe((results) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||
);
|
||||
if (!isNotEmpty(this.configuration$)) {
|
||||
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current paginated search options
|
||||
* @returns {Observable<PaginatedSearchOptions>}
|
||||
*/
|
||||
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
|
||||
return this.searchConfigService.paginatedSearchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a collapsed state
|
||||
*/
|
||||
public closeSidebar(): void {
|
||||
this.sidebarService.collapse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar(): void {
|
||||
this.sidebarService.expand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sidebar is collapsed
|
||||
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||
*/
|
||||
private isSidebarCollapsed(): Observable<boolean> {
|
||||
return this.sidebarService.isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||
*/
|
||||
private getSearchLink(): string {
|
||||
if (this.inPlaceSearch) {
|
||||
return './';
|
||||
}
|
||||
return this.service.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
export class SearchPageComponent {
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { CoreModule } from '../core/core.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SearchPageRoutingModule } from './search-page-routing.module';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchResultsComponent } from './search-results/search-results.component';
|
||||
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
|
||||
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
|
||||
@@ -32,8 +32,10 @@ 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';
|
||||
import { SearchTrackerComponent } from './search-tracker.component';
|
||||
|
||||
const effects = [
|
||||
SidebarEffects
|
||||
@@ -41,6 +43,7 @@ const effects = [
|
||||
|
||||
const components = [
|
||||
SearchPageComponent,
|
||||
SearchComponent,
|
||||
SearchResultsComponent,
|
||||
SearchSidebarComponent,
|
||||
SearchSettingsComponent,
|
||||
@@ -60,8 +63,8 @@ const components = [
|
||||
SearchFacetRangeOptionComponent,
|
||||
SearchSwitchConfigurationComponent,
|
||||
SearchAuthorityFilterComponent,
|
||||
FilteredSearchPageComponent,
|
||||
ConfigurationSearchPageComponent
|
||||
ConfigurationSearchPageComponent,
|
||||
SearchTrackerComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -71,6 +74,7 @@ const components = [
|
||||
SharedModule,
|
||||
EffectsModule.forFeature(effects),
|
||||
CoreModule.forRoot(),
|
||||
StatisticsModule.forRoot(),
|
||||
],
|
||||
declarations: components,
|
||||
providers: [
|
||||
|
@@ -226,7 +226,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
this.getFiltersPart(),
|
||||
).subscribe((update) => {
|
||||
const currentValue: SearchOptions = this.searchOptions.getValue();
|
||||
const updatedValue: SearchOptions = Object.assign(currentValue, update);
|
||||
const updatedValue: SearchOptions = Object.assign(new SearchOptions({}), currentValue, update);
|
||||
this.searchOptions.next(updatedValue);
|
||||
});
|
||||
}
|
||||
@@ -247,7 +247,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
this.getFiltersPart(),
|
||||
).subscribe((update) => {
|
||||
const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue();
|
||||
const updatedValue: PaginatedSearchOptions = Object.assign(currentValue, update);
|
||||
const updatedValue: PaginatedSearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, update);
|
||||
this.paginatedSearchOptions.next(updatedValue);
|
||||
});
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { NavigationExtras, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
|
||||
import { first, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
getSucceededRemoteData
|
||||
} from '../../core/shared/operators';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||
import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
||||
import { SearchOptions } from '../search-options.model';
|
||||
import { SearchResult } from '../search-result.model';
|
||||
@@ -42,6 +42,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { RouteService } from '../../core/services/route.service';
|
||||
import { RequestEntry } from '../../core/data/request.reducer';
|
||||
|
||||
/**
|
||||
* Service that performs all general actions that have to do with the search page
|
||||
@@ -97,9 +98,9 @@ export class SearchService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getEndpoint(searchOptions?: PaginatedSearchOptions): Observable<string> {
|
||||
getEndpoint(searchOptions?:PaginatedSearchOptions):Observable<string> {
|
||||
return this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||
map((url: string) => {
|
||||
map((url:string) => {
|
||||
if (hasValue(searchOptions)) {
|
||||
return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
||||
} else {
|
||||
@@ -116,32 +117,60 @@ export class SearchService implements OnDestroy {
|
||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||
*/
|
||||
search(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
return this.getPaginatedResults(this.searchEntries(searchOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to retrieve request entries for search results from the server
|
||||
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
||||
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
||||
* @returns {Observable<RequestEntry>} Emits an observable with the request entries
|
||||
*/
|
||||
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number)
|
||||
:Observable<{searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry}> {
|
||||
|
||||
const hrefObs = this.getEndpoint(searchOptions);
|
||||
|
||||
const requestObs = hrefObs.pipe(
|
||||
map((url: string) => {
|
||||
map((url:string) => {
|
||||
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||
|
||||
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
|
||||
const getResponseParserFn:() => GenericConstructor<ResponseParsingService> = () => {
|
||||
return this.parser;
|
||||
};
|
||||
|
||||
return Object.assign(request, {
|
||||
responseMsToLive: hasValue(responseMsToLive) ? responseMsToLive : request.responseMsToLive,
|
||||
getResponseParser: getResponseParserFn
|
||||
getResponseParser: getResponseParserFn,
|
||||
searchOptions: searchOptions
|
||||
});
|
||||
}),
|
||||
configureRequest(this.requestService),
|
||||
);
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
return requestObs.pipe(
|
||||
switchMap((request:RestRequest) => this.requestService.getByHref(request.href)),
|
||||
map(((requestEntry:RequestEntry) => ({
|
||||
searchOptions: searchOptions,
|
||||
requestEntry: requestEntry
|
||||
})))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to convert the parsed responses into a paginated list of search results
|
||||
* @param searchEntries: The request entries from the search method
|
||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||
*/
|
||||
getPaginatedResults(searchEntries:Observable<{ searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry }>)
|
||||
:Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
const requestEntryObs:Observable<RequestEntry> = searchEntries.pipe(
|
||||
map((entry) => entry.requestEntry),
|
||||
);
|
||||
|
||||
// get search results from response cache
|
||||
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||
const sqrObs:Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||
filterSuccessfulResponses(),
|
||||
map((response: SearchSuccessResponse) => response.results)
|
||||
map((response:SearchSuccessResponse) => response.results),
|
||||
);
|
||||
|
||||
// turn dspace href from search results to effective list of DSpaceObjects
|
||||
@@ -187,11 +216,12 @@ export class SearchService implements OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
return observableCombineLatest(hrefObs, tDomainListObs, requestEntryObs).pipe(
|
||||
switchMap(([href, tDomainList, requestEntry]) => {
|
||||
return observableCombineLatest(tDomainListObs, searchEntries).pipe(
|
||||
switchMap(([tDomainList, searchEntry]) => {
|
||||
const requestEntry = searchEntry.requestEntry;
|
||||
if (tDomainList.indexOf(undefined) > -1 && requestEntry && requestEntry.completed) {
|
||||
this.requestService.removeByHrefSubstring(href);
|
||||
return this.search(searchOptions)
|
||||
this.requestService.removeByHrefSubstring(requestEntry.request.href);
|
||||
return this.search(searchEntry.searchOptions)
|
||||
} else {
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||
}
|
||||
|
1
src/app/+search-page/search-tracker.component.html
Normal file
1
src/app/+search-page/search-tracker.component.html
Normal file
@@ -0,0 +1 @@
|
||||
|
3
src/app/+search-page/search-tracker.component.scss
Normal file
3
src/app/+search-page/search-tracker.component.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: none
|
||||
}
|
88
src/app/+search-page/search-tracker.component.ts
Normal file
88
src/app/+search-page/search-tracker.component.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { filter, map, switchMap } from 'rxjs/operators';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { SearchQueryResponse } from './search-service/search-query-response.model';
|
||||
import { SearchSuccessResponse } from '../core/cache/response.models';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
|
||||
/**
|
||||
* This component triggers a page view statistic
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-search-tracker',
|
||||
styleUrls: ['./search-tracker.component.scss'],
|
||||
templateUrl: './search-tracker.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class SearchTrackerComponent extends SearchComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
protected service:SearchService,
|
||||
protected sidebarService:SidebarService,
|
||||
protected windowService:HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService:SearchConfigurationService,
|
||||
protected routeService:RouteService,
|
||||
public angulartics2:Angulartics2
|
||||
) {
|
||||
super(service, sidebarService, windowService, searchConfigService, routeService);
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
// super.ngOnInit();
|
||||
this.getSearchOptions().pipe(
|
||||
switchMap((options) => this.service.searchEntries(options)
|
||||
.pipe(
|
||||
filter((entry) =>
|
||||
hasValue(entry.requestEntry)
|
||||
&& hasValue(entry.requestEntry.response)
|
||||
&& entry.requestEntry.response.isSuccessful === true
|
||||
),
|
||||
map((entry) => ({
|
||||
searchOptions: entry.searchOptions,
|
||||
response: (entry.requestEntry.response as SearchSuccessResponse).results
|
||||
})),
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe((entry) => {
|
||||
const config:PaginatedSearchOptions = entry.searchOptions;
|
||||
const searchQueryResponse:SearchQueryResponse = entry.response;
|
||||
const filters:Array<{ filter:string, operator:string, value:string, label:string; }> = [];
|
||||
const appliedFilters = searchQueryResponse.appliedFilters || [];
|
||||
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
|
||||
const appliedFilter = appliedFilters[i];
|
||||
filters.push(appliedFilter);
|
||||
}
|
||||
this.angulartics2.eventTrack.next({
|
||||
action: 'search',
|
||||
properties: {
|
||||
searchOptions: config,
|
||||
page: {
|
||||
size: config.pagination.size, // same as searchQueryResponse.page.elementsPerPage
|
||||
totalElements: searchQueryResponse.page.totalElements,
|
||||
totalPages: searchQueryResponse.page.totalPages,
|
||||
number: config.pagination.currentPage, // same as searchQueryResponse.page.currentPage
|
||||
},
|
||||
sort: {
|
||||
by: config.sort.field,
|
||||
order: config.sort.direction
|
||||
},
|
||||
filters: filters,
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
50
src/app/+search-page/search.component.html
Normal file
50
src/app/+search-page/search.component.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
|
||||
<div class="row">
|
||||
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async"
|
||||
[configuration]="configuration$ | async"
|
||||
[disableHeader]="!searchEnabled"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</ds-page-with-sidebar>
|
||||
|
||||
<ng-template #sidebarContent>
|
||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
>
|
||||
</ds-search-sidebar>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #searchForm>
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
</ng-template>
|
@@ -10,7 +10,7 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
@@ -27,11 +27,11 @@ import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/testing/utils';
|
||||
|
||||
let comp: SearchPageComponent;
|
||||
let fixture: ComponentFixture<SearchPageComponent>;
|
||||
let comp: SearchComponent;
|
||||
let fixture: ComponentFixture<SearchComponent>;
|
||||
let searchServiceObject: SearchService;
|
||||
let searchConfigurationServiceObject: SearchConfigurationService;
|
||||
const store: Store<SearchPageComponent> = jasmine.createSpyObj('store', {
|
||||
const store: Store<SearchComponent> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
dispatch: {},
|
||||
/* tslint:enable:no-empty */
|
||||
@@ -150,14 +150,14 @@ export function configureSearchComponentTestingModule(compType) {
|
||||
}).compileComponents();
|
||||
}
|
||||
|
||||
describe('SearchPageComponent', () => {
|
||||
describe('SearchComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
configureSearchComponentTestingModule(SearchPageComponent);
|
||||
configureSearchComponentTestingModule(SearchComponent);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchPageComponent);
|
||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||
fixture = TestBed.createComponent(SearchComponent);
|
||||
comp = fixture.componentInstance; // SearchComponent test instance
|
||||
fixture.detectChanges();
|
||||
searchServiceObject = (comp as any).service;
|
||||
searchConfigurationServiceObject = (comp as any).searchConfigService;
|
175
src/app/+search-page/search.component.ts
Normal file
175
src/app/+search-page/search.component.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { startWith, switchMap, } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { SearchResult } from './search-result.model';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* This component renders a sidebar, a search input bar and the search results.
|
||||
*/
|
||||
export class SearchComponent implements OnInit {
|
||||
/**
|
||||
* The current search results
|
||||
*/
|
||||
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* The current paginated search options
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
*/
|
||||
scopeListRD$: Observable<DSpaceObject[]>;
|
||||
|
||||
/**
|
||||
* Emits true if were on a small screen
|
||||
*/
|
||||
isXsOrSm$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
/**
|
||||
* True when the search component should show results on the current page
|
||||
*/
|
||||
@Input() inPlaceSearch = true;
|
||||
|
||||
/**
|
||||
* Whether or not the search bar should be visible
|
||||
*/
|
||||
@Input()
|
||||
searchEnabled = true;
|
||||
|
||||
/**
|
||||
* The width of the sidebar (bootstrap columns)
|
||||
*/
|
||||
@Input()
|
||||
sideBarWidth = 3;
|
||||
|
||||
/**
|
||||
* The currently applied configuration (determines title of search)
|
||||
*/
|
||||
@Input()
|
||||
configuration$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Link to the search page
|
||||
*/
|
||||
searchLink: string;
|
||||
|
||||
/**
|
||||
* Observable for whether or not the sidebar is currently collapsed
|
||||
*/
|
||||
isSidebarCollapsed$: Observable<boolean>;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
|
||||
this.searchLink = this.getSearchLink();
|
||||
this.searchOptions$ = this.getSearchOptions();
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined))))
|
||||
.subscribe((results) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||
);
|
||||
if (!isNotEmpty(this.configuration$)) {
|
||||
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current paginated search options
|
||||
* @returns {Observable<PaginatedSearchOptions>}
|
||||
*/
|
||||
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
|
||||
return this.searchConfigService.paginatedSearchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a collapsed state
|
||||
*/
|
||||
public closeSidebar(): void {
|
||||
this.sidebarService.collapse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar(): void {
|
||||
this.sidebarService.expand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sidebar is collapsed
|
||||
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||
*/
|
||||
private isSidebarCollapsed(): Observable<boolean> {
|
||||
return this.sidebarService.isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||
*/
|
||||
private getSearchLink(): string {
|
||||
if (this.inPlaceSearch) {
|
||||
return './';
|
||||
}
|
||||
return this.service.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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' },
|
||||
|
@@ -46,6 +46,7 @@ import { MockActivatedRoute } from './shared/mocks/mock-active-router';
|
||||
import { MockRouter } from './shared/mocks/mock-router';
|
||||
import { MockCookieService } from './shared/mocks/mock-cookie.service';
|
||||
import { CookieService } from './core/services/cookie.service';
|
||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
@@ -74,6 +75,7 @@ describe('App component', () => {
|
||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||
{ provide: MetadataService, useValue: new MockMetadataService() },
|
||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
||||
{ provide: Angulartics2DSpace, useValue: new AngularticsMock() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: new MockRouter() },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
|
@@ -34,6 +34,7 @@ import { HostWindowService } from './shared/host-window.service';
|
||||
import { Theme } from '../config/theme.inferface';
|
||||
import { isNotEmpty } from './shared/empty.util';
|
||||
import { CookieService } from './core/services/cookie.service';
|
||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
|
||||
export const LANG_COOKIE = 'language_cookie';
|
||||
|
||||
@@ -60,6 +61,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
private store: Store<HostWindowState>,
|
||||
private metadata: MetadataService,
|
||||
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private angulartics2DSpace: Angulartics2DSpace,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private cssService: CSSVariableService,
|
||||
@@ -89,6 +91,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
angulartics2DSpace.startTracking();
|
||||
|
||||
metadata.listenForRouteChange();
|
||||
|
||||
if (config.debug) {
|
||||
|
@@ -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;
|
||||
|
40
src/app/community-list-page/community-list-datasource.ts
Normal file
40
src/app/community-list-page/community-list-datasource.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<div class="container">
|
||||
<h2>{{ 'communityList.title' | translate }}</h2>
|
||||
<ds-community-list></ds-community-list>
|
||||
</div>
|
@@ -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();
|
||||
}));
|
||||
|
||||
});
|
13
src/app/community-list-page/community-list-page.component.ts
Normal file
13
src/app/community-list-page/community-list-page.component.ts
Normal 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 {
|
||||
|
||||
}
|
26
src/app/community-list-page/community-list-page.module.ts
Normal file
26
src/app/community-list-page/community-list-page.module.ts
Normal 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 {
|
||||
|
||||
}
|
@@ -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 {
|
||||
}
|
574
src/app/community-list-page/community-list-service.spec.ts
Normal file
574
src/app/community-list-page/community-list-service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
335
src/app/community-list-page/community-list-service.ts
Normal file
335
src/app/community-list-page/community-list-service.ts
Normal 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$;
|
||||
}
|
||||
|
||||
}
|
35
src/app/community-list-page/community-list.actions.ts
Normal file
35
src/app/community-list-page/community-list.actions.ts
Normal 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;
|
45
src/app/community-list-page/community-list.reducer.spec.ts
Normal file
45
src/app/community-list-page/community-list.reducer.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
36
src/app/community-list-page/community-list.reducer.ts
Normal file
36
src/app/community-list-page/community-list.reducer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -65,7 +65,7 @@ export class NormalizedItem extends NormalizedDSpaceObject<Item> {
|
||||
@relationship(Bundle, true)
|
||||
bundles: string[];
|
||||
|
||||
@autoserialize
|
||||
@deserialize
|
||||
@relationship(Relationship, true)
|
||||
relationships: string[];
|
||||
|
||||
|
13
src/app/core/cache/models/normalized-site.model.ts
vendored
Normal file
13
src/app/core/cache/models/normalized-site.model.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { inheritSerialization } from 'cerialize';
|
||||
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
||||
import { mapsTo } from '../builders/build-decorators';
|
||||
import { Site } from '../../shared/site.model';
|
||||
|
||||
/**
|
||||
* Normalized model class for a Site object
|
||||
*/
|
||||
@mapsTo(Site)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedSite extends NormalizedDSpaceObject<Site> {
|
||||
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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';
|
||||
|
@@ -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)),
|
||||
|
@@ -121,6 +121,8 @@ import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
||||
import { BrowseDefinition } from './shared/browse-definition.model';
|
||||
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||
import { SiteDataService } from './data/site-data.service';
|
||||
import { NormalizedSite } from './cache/models/normalized-site.model';
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -139,6 +141,7 @@ const PROVIDERS = [
|
||||
AuthResponseParsingService,
|
||||
CommunityDataService,
|
||||
CollectionDataService,
|
||||
SiteDataService,
|
||||
DSOResponseParsingService,
|
||||
DSpaceRESTv2Service,
|
||||
DynamicFormLayoutService,
|
||||
@@ -232,6 +235,7 @@ export const normalizedModels =
|
||||
NormalizedBitstream,
|
||||
NormalizedBitstreamFormat,
|
||||
NormalizedItem,
|
||||
NormalizedSite,
|
||||
NormalizedCollection,
|
||||
NormalizedCommunity,
|
||||
NormalizedEPerson,
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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}`)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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) => {
|
||||
|
@@ -1,5 +1,14 @@
|
||||
import { distinctUntilChanged, filter, map, mergeMap, share, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { merge as observableMerge, Observable, throwError as observableThrowError, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter, first,
|
||||
map,
|
||||
mergeMap,
|
||||
share,
|
||||
switchMap,
|
||||
take,
|
||||
tap
|
||||
} from 'rxjs/operators';
|
||||
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
@@ -7,6 +16,9 @@ import { CommunityDataService } from './community-data.service';
|
||||
|
||||
import { DataService } from './data.service';
|
||||
import { DeleteRequest, FindAllOptions, FindByIDRequest, RestRequest } 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 {
|
||||
configureRequest,
|
||||
@@ -39,7 +51,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 {
|
||||
@@ -71,6 +83,13 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for the community or collection's logo
|
||||
* @param id The community or collection's ID
|
||||
|
@@ -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}`))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
});
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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: '',
|
||||
|
@@ -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}`))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,10 @@ export class SearchResponseParsingService implements ResponseParsingService {
|
||||
}
|
||||
};
|
||||
const payload = data.payload._embedded.searchResult || emptyPayload;
|
||||
payload.appliedFilters = data.payload.appliedFilters;
|
||||
payload.sort = data.payload.sort;
|
||||
payload.scope = data.payload.scope;
|
||||
payload.configuration = data.payload.configuration;
|
||||
const hitHighlights: MetadataMap[] = payload._embedded.objects
|
||||
.map((object) => object.hitHighlights)
|
||||
.map((hhObject) => {
|
||||
|
104
src/app/core/data/site-data.service.spec.ts
Normal file
104
src/app/core/data/site-data.service.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { SiteDataService } from './site-data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { Site } from '../shared/site.model';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
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 { FindListOptions } from './request.models';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { RemoteData } from './remote-data';
|
||||
|
||||
describe('SiteDataService', () => {
|
||||
let scheduler:TestScheduler;
|
||||
let service:SiteDataService;
|
||||
let halService:HALEndpointService;
|
||||
let requestService:RequestService;
|
||||
let rdbService:RemoteDataBuildService;
|
||||
let objectCache:ObjectCacheService;
|
||||
|
||||
const testObject = Object.assign(new Site(), {
|
||||
uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746',
|
||||
});
|
||||
|
||||
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||
const options = Object.assign(new FindListOptions(), {});
|
||||
|
||||
const getRequestEntry$ = (successful:boolean, statusCode:number, statusText:string) => {
|
||||
return observableOf({
|
||||
response: new RestResponse(successful, statusCode, statusText)
|
||||
} as RequestEntry);
|
||||
};
|
||||
|
||||
const siteLink = 'https://rest.api/rest/api/config/sites';
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', {a: siteLink})
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
configure: true,
|
||||
getByHref: getRequestEntry$(true, 200, 'Success')
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildList: cold('a', {
|
||||
a: new RemoteData(false, false, true, undefined, new PaginatedList(null, [testObject]))
|
||||
})
|
||||
});
|
||||
|
||||
const store = {} as Store<CoreState>;
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||
|
||||
service = new SiteDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
dataBuildService,
|
||||
store,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
http,
|
||||
comparator
|
||||
);
|
||||
});
|
||||
|
||||
describe('getBrowseEndpoint', () => {
|
||||
it('should return the Static Page endpoint', () => {
|
||||
|
||||
const result = service.getBrowseEndpoint(options);
|
||||
const expected = cold('b', {b: siteLink});
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('find', () => {
|
||||
it('should return the Site object', () => {
|
||||
|
||||
spyOn(service, 'findAll').and.returnValue(cold('a', {
|
||||
a: new RemoteData(false, false, true, undefined, new PaginatedList(null, [testObject]))
|
||||
}));
|
||||
|
||||
const expected = cold('(b|)', {b: testObject});
|
||||
const result = service.find();
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
68
src/app/core/data/site-data.service.ts
Normal file
68
src/app/core/data/site-data.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { DataService } from './data.service';
|
||||
import { Site } from '../shared/site.model';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
import { FindListOptions } from './request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { getSucceededRemoteData } from '../shared/operators';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the Site object
|
||||
*/
|
||||
@Injectable()
|
||||
export class SiteDataService extends DataService<Site> {
|
||||
|
||||
protected linkPath = 'sites';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
|
||||
constructor(
|
||||
protected requestService:RequestService,
|
||||
protected rdbService:RemoteDataBuildService,
|
||||
protected dataBuildService:NormalizedObjectBuildService,
|
||||
protected store:Store<CoreState>,
|
||||
protected objectCache:ObjectCacheService,
|
||||
protected halService:HALEndpointService,
|
||||
protected notificationsService:NotificationsService,
|
||||
protected http:HttpClient,
|
||||
protected comparator:DSOChangeAnalyzer<Site>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing the site object
|
||||
* @param {FindListOptions} options
|
||||
* @param {Observable<string>} linkPath
|
||||
*/
|
||||
getBrowseEndpoint(options:FindListOptions, linkPath?:string):Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the Site Object
|
||||
*/
|
||||
find():Observable<Site> {
|
||||
return this.findAll().pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((remoteData:RemoteData<PaginatedList<Site>>) => remoteData.payload),
|
||||
map((list:PaginatedList<Site>) => list.page[0])
|
||||
);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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(
|
||||
|
11
src/app/core/shared/site.model.ts
Normal file
11
src/app/core/shared/site.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* Model class for the Site object
|
||||
*/
|
||||
export class Site extends DSpaceObject {
|
||||
|
||||
static type = new ResourceType('site');
|
||||
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { SubmissionService } from '../../submission/submission.service';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { SubmissionObject } from './models/submission-object.model';
|
||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||
import { SubmissionObjectDataService } from './submission-object-data.service';
|
||||
import { SubmissionScopeType } from './submission-scope-type';
|
||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||
|
||||
describe('SubmissionObjectDataService', () => {
|
||||
let service: SubmissionObjectDataService;
|
||||
let submissionService: SubmissionService;
|
||||
let workspaceitemDataService: WorkspaceitemDataService;
|
||||
let workflowItemDataService: WorkflowItemDataService;
|
||||
|
||||
const submissionId = '1234';
|
||||
const wsiResult = 'wsiResult' as any;
|
||||
const wfiResult = 'wfiResult' as any;
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceitemDataService = jasmine.createSpyObj('WorkspaceitemDataService', {
|
||||
findById: wsiResult
|
||||
});
|
||||
workflowItemDataService = jasmine.createSpyObj('WorkflowItemDataService', {
|
||||
findById: wfiResult
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should call SubmissionService.getSubmissionScope to determine the type of submission object', () => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: {}
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
service.findById(submissionId);
|
||||
expect(submissionService.getSubmissionScope).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when the submission ID refers to a WorkspaceItem', () => {
|
||||
beforeEach(() => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: SubmissionScopeType.WorkspaceItem
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
});
|
||||
|
||||
it('should forward the result of WorkspaceitemDataService.findById()', () => {
|
||||
const result = service.findById(submissionId);
|
||||
expect(workspaceitemDataService.findById).toHaveBeenCalledWith(submissionId);
|
||||
expect(result).toBe(wsiResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the submission ID refers to a WorkflowItem', () => {
|
||||
beforeEach(() => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: SubmissionScopeType.WorkflowItem
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
});
|
||||
|
||||
it('should forward the result of WorkflowItemDataService.findById()', () => {
|
||||
const result = service.findById(submissionId);
|
||||
expect(workflowItemDataService.findById).toHaveBeenCalledWith(submissionId);
|
||||
expect(result).toBe(wfiResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the type of submission object is unknown', () => {
|
||||
beforeEach(() => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: 'Something else'
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
});
|
||||
|
||||
it('shouldn\'t call any data service methods', () => {
|
||||
service.findById(submissionId);
|
||||
expect(workspaceitemDataService.findById).not.toHaveBeenCalled();
|
||||
expect(workflowItemDataService.findById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return a RemoteData containing an error', (done) => {
|
||||
const result = service.findById(submissionId);
|
||||
result.subscribe((rd: RemoteData<SubmissionObject>) => {
|
||||
expect(rd.hasFailed).toBe(true);
|
||||
expect(rd.error).toBeDefined();
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
46
src/app/core/submission/submission-object-data.service.ts
Normal file
46
src/app/core/submission/submission-object-data.service.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { of as observableOf, Observable } from 'rxjs';
|
||||
import { SubmissionService } from '../../submission/submission.service';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RemoteDataError } from '../data/remote-data-error';
|
||||
import { SubmissionObject } from './models/submission-object.model';
|
||||
import { SubmissionScopeType } from './submission-scope-type';
|
||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||
|
||||
/**
|
||||
* A service to retrieve submission objects (WorkspaceItem/WorkflowItem)
|
||||
* without knowing their type
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SubmissionObjectDataService {
|
||||
constructor(
|
||||
private workspaceitemDataService: WorkspaceitemDataService,
|
||||
private workflowItemDataService: WorkflowItemDataService,
|
||||
private submissionService: SubmissionService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a submission object based on its ID.
|
||||
*
|
||||
* @param id The identifier of a submission object
|
||||
*/
|
||||
findById(id: string): Observable<RemoteData<SubmissionObject>> {
|
||||
switch (this.submissionService.getSubmissionScope()) {
|
||||
case SubmissionScopeType.WorkspaceItem:
|
||||
return this.workspaceitemDataService.findById(id);
|
||||
case SubmissionScopeType.WorkflowItem:
|
||||
return this.workflowItemDataService.findById(id);
|
||||
default:
|
||||
const error = new RemoteDataError(
|
||||
undefined,
|
||||
undefined,
|
||||
'The request couldn\'t be sent. Unable to determine the type of submission object'
|
||||
);
|
||||
return observableOf(new RemoteData(false, false, false, error, undefined));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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 */
|
||||
{
|
||||
|
@@ -112,6 +112,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
||||
repeatable: false
|
||||
}),
|
||||
new DynamicRelationGroupModel({
|
||||
submissionId: '1234',
|
||||
id: 'relationGroup',
|
||||
formConfiguration: [],
|
||||
mandatoryField: '',
|
||||
|
@@ -33,6 +33,8 @@ export let FORM_GROUP_TEST_GROUP;
|
||||
|
||||
const config: GlobalConfig = MOCK_SUBMISSION_CONFIG;
|
||||
|
||||
const submissionId = '1234';
|
||||
|
||||
function init() {
|
||||
FORM_GROUP_TEST_MODEL_CONFIG = {
|
||||
disabled: false,
|
||||
@@ -67,6 +69,7 @@ function init() {
|
||||
}]
|
||||
} as FormFieldModel]
|
||||
} as FormRowModel],
|
||||
submissionId,
|
||||
id: 'dc_contributor_author',
|
||||
label: 'Authors',
|
||||
mandatoryField: 'dc.contributor.author',
|
||||
@@ -183,7 +186,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => {
|
||||
|
||||
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
const formConfig = { rows: groupComp.model.formConfiguration } as SubmissionFormsModel;
|
||||
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const formModel = service.modelFromConfiguration(submissionId, formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const chips = new Chips([], 'value', 'dc.contributor.author');
|
||||
groupComp.formCollapsed.subscribe((value) => {
|
||||
expect(value).toEqual(false);
|
||||
@@ -257,7 +260,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => {
|
||||
|
||||
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
const formConfig = { rows: groupComp.model.formConfiguration } as SubmissionFormsModel;
|
||||
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const formModel = service.modelFromConfiguration(submissionId, formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const chips = new Chips(modelValue, 'value', 'dc.contributor.author');
|
||||
groupComp.formCollapsed.subscribe((value) => {
|
||||
expect(value).toEqual(true);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user