Skip to content

Commit 2120cff

Browse files
[UXP-171] Port of [DSC-1650] refactor of browse-most-elements component
1 parent 582d95c commit 2120cff

20 files changed

Lines changed: 348 additions & 250 deletions

config/config.example.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ browseBy:
277277
defaultLowerLimit: 1900
278278
# If true, thumbnail images for items will be added to BOTH search and browse result lists.
279279
showThumbnails: true
280+
# Whether to add item thumbnail images to BOTH browse and search result lists.
281+
showMetrics: false
280282
# The number of entries in a paginated browse results list.
281283
# Rounded to the nearest size in the list of selectable sizes on the
282284
# settings menu.

src/app/core/layout/models/section.model.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface TopSection extends SectionComponent {
6767
itemListStyle?: string;
6868
cardColumnStyle?: string;
6969
showAllResults: boolean;
70+
template: TopSectionTemplateType;
7071
}
7172

7273
export interface GridSection extends SectionComponent {
@@ -109,6 +110,14 @@ export interface TopSectionColumn {
109110
titleKey: string;
110111
}
111112

113+
/**
114+
* Represents the type of template to use for the section
115+
*/
116+
export enum TopSectionTemplateType {
117+
DEFAULT = 'default', // CRIS default template
118+
CARD = 'card', // Card template
119+
}
120+
112121
export enum LayoutModeEnum {
113122
LIST = 'list',
114123
CARD = 'card'

src/app/explore-page/explore-page.component.spec.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
1-
import { CommonModule } from '@angular/common';
2-
import { NO_ERRORS_SCHEMA } from '@angular/core';
3-
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
4-
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
5-
import { BrowserModule, By } from '@angular/platform-browser';
6-
import { ActivatedRoute } from '@angular/router';
7-
import { RouterTestingModule } from '@angular/router/testing';
8-
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
9-
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
10-
import { Observable, of } from 'rxjs';
11-
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
12-
import { RemoteData } from '../core/data/remote-data';
13-
import { BrowseSection, FacetSection, LayoutModeEnum, SearchSection, Section, TopSection } from '../core/layout/models/section.model';
14-
import { SectionDataService } from '../core/layout/section-data.service';
15-
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
16-
import { ExplorePageComponent } from './explore-page.component';
1+
import {CommonModule} from '@angular/common';
2+
import {NO_ERRORS_SCHEMA} from '@angular/core';
3+
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
4+
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
5+
import {BrowserModule, By} from '@angular/platform-browser';
6+
import {ActivatedRoute} from '@angular/router';
7+
import {RouterTestingModule} from '@angular/router/testing';
8+
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
9+
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
10+
import {Observable, of} from 'rxjs';
11+
import {TranslateLoaderMock} from '../shared/mocks/translate-loader.mock';
12+
import {RemoteData} from '../core/data/remote-data';
13+
import {
14+
BrowseSection,
15+
FacetSection, LayoutModeEnum,
16+
SearchSection,
17+
Section,
18+
TopSection,
19+
TopSectionTemplateType
20+
} from '../core/layout/models/section.model';
21+
import {SectionDataService} from '../core/layout/section-data.service';
22+
import {createSuccessfulRemoteDataObject$} from '../shared/remote-data.utils';
23+
import {ExplorePageComponent} from './explore-page.component';
1724

1825
describe('ExploreComponent', () => {
1926
let component: ExplorePageComponent;
@@ -41,6 +48,7 @@ describe('ExploreComponent', () => {
4148
showLayoutSwitch: true,
4249
defaultLayoutMode: LayoutModeEnum.LIST,
4350
showAllResults: true,
51+
template: TopSectionTemplateType.DEFAULT
4452
};
4553

4654
const searchComponent: SearchSection = {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { followLink } from '../utils/follow-link-config.model';
2+
import {TopSection} from '../../core/layout/models/section.model';
3+
import { Component, Input, OnChanges, OnInit, PLATFORM_ID, inject } from '@angular/core';
4+
import { isPlatformServer } from '@angular/common';
5+
6+
import { SearchService } from '../../core/shared/search/search.service';
7+
import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model';
8+
import { DSpaceObject } from '../../core/shared/dspace-object.model';
9+
import { SearchResult } from '../search/models/search-result.model';
10+
import { Context } from '../../core/shared/context.model';
11+
import { RemoteData } from '../../core/data/remote-data';
12+
import { PaginatedList } from '../../core/data/paginated-list.model';
13+
import {
14+
getAllCompletedRemoteData,
15+
getPaginatedListPayload,
16+
getRemoteDataPayload,
17+
toDSpaceObjectListRD,
18+
} from '../../core/shared/operators';
19+
import { APP_CONFIG } from '../../../config/app-config.interface';
20+
import { BehaviorSubject, Observable, mergeMap } from 'rxjs';
21+
import { Item } from '../../core/shared/item.model';
22+
import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
23+
24+
@Component({
25+
template: ''
26+
})
27+
export abstract class AbstractBrowseElementsComponent implements OnInit, OnChanges {
28+
29+
protected readonly appConfig = inject(APP_CONFIG);
30+
protected readonly platformId = inject(PLATFORM_ID);
31+
protected readonly searchService = inject(SearchService);
32+
33+
protected followThumbnailLink: boolean; // to be overridden
34+
35+
@Input() paginatedSearchOptions: PaginatedSearchOptions;
36+
37+
@Input() context: Context;
38+
39+
@Input() topSection: TopSection;
40+
41+
paginatedSearchOptionsBS: BehaviorSubject<PaginatedSearchOptions>;
42+
43+
searchResults$: Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>;
44+
45+
searchResultArray$: Observable<DSpaceObject[]>;
46+
47+
ngOnChanges() {
48+
if (isPlatformServer(this.platformId)) {
49+
return;
50+
}
51+
this.paginatedSearchOptionsBS?.next(this.paginatedSearchOptions);
52+
}
53+
54+
ngOnInit() {
55+
const followLinks = this.followThumbnailLink ? [followLink('thumbnail')] : [];
56+
this.paginatedSearchOptionsBS = new BehaviorSubject<PaginatedSearchOptions>(this.paginatedSearchOptions);
57+
this.searchResults$ = this.paginatedSearchOptionsBS.asObservable().pipe(
58+
mergeMap((paginatedSearchOptions) =>
59+
this.searchService.search(paginatedSearchOptions, null, true, true, ...followLinks),
60+
),
61+
getAllCompletedRemoteData(),
62+
);
63+
64+
this.searchResultArray$ = this.searchResults$.pipe(
65+
toDSpaceObjectListRD(),
66+
getRemoteDataPayload(),
67+
getPaginatedListPayload(),
68+
);
69+
}
70+
71+
getItemPageRoute(item: DSpaceObject | Item) {
72+
return getItemPageRoute(item as Item);
73+
}
74+
}
Lines changed: 19 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,21 @@
1-
<div class="m-2">
2-
<ng-container *ngIf="searchResults">
3-
<ng-container
4-
*ngIf="
5-
topSection && mode == cardLayoutMode;
6-
else listLayout
7-
"
8-
>
9-
<ds-object-grid
10-
[configStyle]="topSection?.cardStyle"
11-
[columnStyle]="topSection?.cardColumnStyle"
12-
[config]="paginatedSearchOptions.pagination"
13-
[sortConfig]="paginatedSearchOptions.sort"
14-
[objects]="searchResults"
15-
[linkType]="collectionElementLinkTypeEnum.Link"
16-
[context]="context"
17-
[noPagination]="true"
18-
>
19-
</ds-object-grid>
20-
</ng-container>
21-
<ng-template #listLayout>
22-
<ul *ngIf="searchResults?.hasSucceeded" class="list-unstyled">
23-
<li
24-
*ngFor="
25-
let object of searchResults?.payload?.page;
26-
let i = index;
27-
let last = last
28-
"
29-
class="mt-4 mb-4 d-flex"
30-
[ngClass]="topSection?.itemListStyle"
31-
>
32-
<ds-listable-object-component-loader [context]="context"
33-
[index]="i"
34-
[listID]="paginatedSearchOptions.configuration"
35-
[object]="object"
36-
[showMetrics]="showMetrics"
37-
[showThumbnails]="showThumbnails"
38-
[viewMode]="paginatedSearchOptions.view"></ds-listable-object-component-loader>
39-
</li>
40-
</ul>
41-
</ng-template>
42-
<div
43-
class="row justify-content-center"
44-
*ngIf="topSection && topSection.showAllResults"
45-
>
46-
<button type="button" class="btn btn-link" (click)="showAllResults()">
47-
{{ "home.top-section.view-results" | translate }}
48-
</button>
49-
</div>
50-
<div *ngIf="searchResults?.hasFailed">
51-
{{ "remote.error" | translate }}
52-
</div>
1+
<div [ngSwitch]="(sectionTemplateType | lowercase)">
2+
3+
<ng-container *ngSwitchDefault>
4+
<ds-themed-default-browse-elements
5+
[showThumbnails]="topSection.showThumbnails"
6+
[topSection]="topSection"
7+
[paginatedSearchOptions]="paginatedSearchOptionsBS.asObservable() | async"
8+
[context]="context"
9+
[mode]="mode"
10+
></ds-themed-default-browse-elements>
5311
</ng-container>
54-
<ds-loading *ngIf="!searchResults"></ds-loading>
12+
13+
</div>
14+
<div
15+
class="row justify-content-center"
16+
*ngIf="topSection && topSection.showAllResults"
17+
>
18+
<button type="button" class="btn btn-link" (click)="showAllResults()">
19+
{{ "home.top-section.view-results" | translate }}
20+
</button>
5521
</div>
Lines changed: 35 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2-
import { ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
3-
4-
import { of } from 'rxjs';
2+
import { NO_ERRORS_SCHEMA } from '@angular/core';
53

64
import { BrowseMostElementsComponent } from './browse-most-elements.component';
7-
import { SearchManager } from '../../core/browse/search-manager';
8-
import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model';
9-
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
10-
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
11-
import { APP_CONFIG } from '../../../config/app-config.interface';
5+
import { TopSectionTemplateType } from '../../core/layout/models/section.model';
6+
import { By } from '@angular/platform-browser';
127
import { ItemSearchResult } from '../object-collection/shared/item-search-result.model';
138
import { Item } from '../../core/shared/item.model';
14-
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
15-
import { buildPaginatedList } from '../../core/data/paginated-list.model';
16-
import { PageInfo } from '../../core/shared/page-info.model';
17-
import { followLink } from '../utils/follow-link-config.model';
18-
import { Router } from '@angular/router';
19-
import { RouterStub } from '../testing/router.stub';
9+
import { of } from 'rxjs';
2010

2111
describe('BrowseMostElementsComponent', () => {
2212
let component: BrowseMostElementsComponent;
@@ -55,92 +45,60 @@ describe('BrowseMostElementsComponent', () => {
5545
]
5646
}
5747
});
58-
const mockResponse = createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), [mockResultObject]));
59-
60-
const mockSearchService = {
61-
search: jasmine.createSpy('search').and.returnValue(of(mockResponse)), // Replace with your desired response
62-
};
63-
64-
const mockConfig = {
65-
browseBy: {
66-
showThumbnails: true
67-
}
68-
};
6948

7049
beforeEach(waitForAsync(() => {
7150
TestBed.configureTestingModule({
7251
declarations: [BrowseMostElementsComponent],
7352
providers: [
74-
{ provide: APP_CONFIG, useValue: mockConfig },
75-
{ provide: SearchManager, useValue: mockSearchService },
76-
{ provide: ChangeDetectorRef, useValue: {} },
77-
{ provide: Router, useValue: new RouterStub() },
7853
],
79-
schemas: [CUSTOM_ELEMENTS_SCHEMA], // Ignore unknown Angular elements
54+
schemas: [NO_ERRORS_SCHEMA],
8055
}).compileComponents();
8156
}));
8257

8358
beforeEach(() => {
8459
fixture = TestBed.createComponent(BrowseMostElementsComponent);
8560
component = fixture.componentInstance;
86-
component.paginatedSearchOptions = new PaginatedSearchOptions({
87-
configuration: 'test',
88-
pagination: Object.assign(new PaginationComponentOptions(), {
89-
id: 'search-object-pagination',
90-
pageSize: 5,
91-
currentPage: 1
92-
}),
93-
sort: new SortOptions('dc.title', SortDirection.ASC)
94-
});
61+
component.topSection = {
62+
template: TopSectionTemplateType.DEFAULT
63+
} as any;
64+
fixture.detectChanges();
9565
});
9666

9767
it('should create', () => {
98-
component.showThumbnails = true;
9968
fixture.detectChanges();
10069
expect(component).toBeTruthy();
10170
});
10271

103-
it('should call searchService.search on ngOnInit with followLinks', () => {
104-
component.showThumbnails = true;
105-
fixture.detectChanges();
106-
107-
expect(mockSearchService.search).toHaveBeenCalledWith(
108-
component.paginatedSearchOptions,
109-
null,
110-
true,
111-
true,
112-
followLink('thumbnail')
113-
);
114-
});
115-
116-
it('should call searchService.search on ngOnInit with followLinks', () => {
117-
component.showThumbnails = undefined;
118-
fixture.detectChanges();
119-
120-
expect(mockSearchService.search).toHaveBeenCalledWith(
121-
component.paginatedSearchOptions,
122-
null,
123-
true,
124-
true,
125-
followLink('thumbnail')
126-
);
127-
});
72+
describe('when the templateType is DEFAULT', () => {
73+
beforeEach(() => {
74+
component.topSection = {
75+
template: TopSectionTemplateType.DEFAULT
76+
} as any;
77+
fixture.detectChanges();
78+
});
12879

129-
it('should call searchService.search on ngOnInit without followLinks', () => {
130-
component.showThumbnails = false;
131-
fixture.detectChanges();
80+
it('should display ds-themed-default-browse-elements', () => {
81+
const defaultElement = fixture.debugElement.query(By.css('ds-themed-default-browse-elements'));
82+
expect(defaultElement).toBeTruthy();
83+
});
13284

133-
expect(mockSearchService.search).toHaveBeenCalledWith(
134-
component.paginatedSearchOptions,
135-
null,
136-
true,
137-
true
138-
);
85+
it('should not display ds-themed-images-browse-elements', () => {
86+
const imageElement = fixture.debugElement.query(By.css('ds-themed-images-browse-elements'));
87+
expect(imageElement).toBeNull();
88+
});
13989
});
14090

141-
it('should update searchResults after searchService response', () => {
142-
component.ngOnInit();
91+
describe('when the templateType is not recognized', () => {
92+
beforeEach(() => {
93+
component.topSection = {
94+
template: 'not recognized' as any
95+
} as any;
96+
fixture.detectChanges();
97+
});
14398

144-
expect(component.searchResults).toEqual(mockResponse);
99+
it('should display ds-themed-default-browse-elements', () => {
100+
const defaultElement = fixture.debugElement.query(By.css('ds-themed-default-browse-elements'));
101+
expect(defaultElement).toBeTruthy();
102+
});
145103
});
146104
});

0 commit comments

Comments
 (0)