diff --git a/config/config.example.yml b/config/config.example.yml index c1d7f967a4..6674bc5334 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -25,6 +25,14 @@ ssr: inlineCriticalCss: false # Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects. paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ] + # Whether to enable rendering of Search component on SSR. + # If set to true the component will be included in the HTML returned from the server side rendering. + # If set to false the component will not be included in the HTML returned from the server side rendering. + enableSearchComponent: false, + # Whether to enable rendering of Browse component on SSR. + # If set to true the component will be included in the HTML returned from the server side rendering. + # If set to false the component will not be included in the HTML returned from the server side rendering. + enableBrowseComponent: false, # The REST API server settings # NOTE: these settings define which (publicly available) REST API to use. They are usually @@ -84,7 +92,7 @@ cache: anonymousCache: # Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled. # As all pages are cached in server memory, increasing this value will increase memory needs. - # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. + # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. max: 0 # Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached # copy is automatically refreshed on the next request. @@ -394,7 +402,7 @@ vocabularies: vocabulary: 'srsc' enabled: true -# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. +# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. comcolSelectionSort: sortField: 'dc.title' sortDirection: 'ASC' @@ -412,3 +420,12 @@ liveRegion: messageTimeOutDurationMs: 30000 # The visibility of the live region. Setting this to true is only useful for debugging purposes. isVisible: false + + +# Search settings +search: + # Number used to render n UI elements called loading skeletons that act as placeholders. + # These elements indicate that some content will be loaded in their stead. + # Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count. + # e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved. + defaultFiltersCount: 5 diff --git a/package.json b/package.json index e94785d93e..0a8849d085 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "ngx-infinite-scroll": "^15.0.0", "ngx-pagination": "6.0.3", "ngx-sortablejs": "^11.1.0", + "ngx-skeleton-loader": "^7.0.0", "ngx-ui-switch": "^14.1.0", "nouislider": "^15.8.1", "pem": "1.14.8", diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts index b19250edae..f64091e41f 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts +++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts @@ -1,5 +1,5 @@ import { BrowseByDatePageComponent } from './browse-by-date-page.component'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; @@ -9,7 +9,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { BrowseService } from '../../core/browse/browse.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { RouterMock } from '../../shared/mocks/router.mock'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectorRef, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core'; import { of as observableOf } from 'rxjs'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { Community } from '../../core/shared/community.model'; @@ -24,6 +24,8 @@ import { APP_CONFIG } from '../../../config/app-config.interface'; import { environment } from '../../../environments/environment'; import { SortDirection } from '../../core/cache/models/sort-options.model'; import { cold } from 'jasmine-marbles'; +import { Store } from '@ngrx/store'; +import { BrowseEntry } from '../../core/shared/browse-entry.model'; describe('BrowseByDatePageComponent', () => { let comp: BrowseByDatePageComponent; @@ -95,7 +97,10 @@ describe('BrowseByDatePageComponent', () => { { provide: Router, useValue: new RouterMock() }, { provide: PaginationService, useValue: paginationService }, { provide: ChangeDetectorRef, useValue: mockCdRef }, - { provide: APP_CONFIG, useValue: environment } + { provide: APP_CONFIG, useValue: environment }, + { provide: Store, useValue: {} }, + { provide: APP_CONFIG, useValue: environment }, + { provide: PLATFORM_ID, useValue: 'browser' }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -131,4 +136,33 @@ describe('BrowseByDatePageComponent', () => { //expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear()); expect(comp.startsWithOptions[0]).toEqual(1960); }); + + describe('when rendered in SSR', () => { + beforeEach(() => { + comp.platformId = 'server'; + spyOn((comp as any).browseService, 'getBrowseItemsFor'); + }); + + it('should not call getBrowseItemsFor on init', (done) => { + comp.ngOnInit(); + expect((comp as any).browseService.getBrowseItemsFor).not.toHaveBeenCalled(); + comp.loading$.subscribe((res) => { + expect(res).toBeFalsy(); + done(); + }); + }); + }); + + describe('when rendered in CSR', () => { + beforeEach(() => { + comp.platformId = 'browser'; + spyOn((comp as any).browseService, 'getBrowseItemsFor').and.returnValue(createSuccessfulRemoteDataObject$(new BrowseEntry())); + }); + + it('should call getBrowseItemsFor on init', fakeAsync(() => { + comp.ngOnInit(); + tick(100); + expect((comp as any).browseService.getBrowseItemsFor).toHaveBeenCalled(); + })); + }); }); diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts index 7e0b6f0f88..afb58e6ad2 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, PLATFORM_ID } from '@angular/core'; import { BrowseByMetadataPageComponent, browseParamsToOptions @@ -11,6 +11,7 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { PaginationService } from '../../core/pagination/pagination.service'; import { map, take } from 'rxjs/operators'; +import { of as observableOf } from 'rxjs'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { isValidDate } from '../../shared/date.util'; @@ -18,6 +19,8 @@ import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { isPlatformServer } from '@angular/common'; +import { environment } from '../../../environments/environment'; @Component({ selector: 'ds-browse-by-date-page', @@ -44,11 +47,16 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { protected cdRef: ChangeDetectorRef, @Inject(APP_CONFIG) public appConfig: AppConfig, public dsoNameService: DSONameService, + @Inject(PLATFORM_ID) public platformId: any, ) { - super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService); + super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService, platformId); } ngOnInit(): void { + if (!this.renderOnServerSide && !environment.universal.enableBrowseComponent && isPlatformServer(this.platformId)) { + this.loading$ = observableOf(false); + return; + } const sortConfig = new SortOptions('default', SortDirection.ASC); this.startsWithType = StartsWithType.date; this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html index a6abd5a7a4..9628de44b2 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts index 2bdecc2670..4ea68eaa0c 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts @@ -3,7 +3,7 @@ import { browseParamsToOptions, getBrowseSearchOptions } from './browse-by-metadata-page.component'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { BrowseService } from '../../core/browse/browse.service'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; @@ -13,7 +13,7 @@ import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe'; import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { Observable, of as observableOf } from 'rxjs'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core'; import { RemoteData } from '../../core/data/remote-data'; import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { PageInfo } from '../../core/shared/page-info.model'; @@ -111,7 +111,8 @@ describe('BrowseByMetadataPageComponent', () => { { provide: DSpaceObjectDataService, useValue: mockDsoService }, { provide: PaginationService, useValue: paginationService }, { provide: Router, useValue: new RouterMock() }, - { provide: APP_CONFIG, useValue: environmentMock } + { provide: APP_CONFIG, useValue: environmentMock }, + { provide: PLATFORM_ID, useValue: 'browser' }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -224,6 +225,35 @@ describe('BrowseByMetadataPageComponent', () => { expect(result.fetchThumbnail).toBeTrue(); }); }); + + describe('when rendered in SSR', () => { + beforeEach(() => { + comp.ssrRenderingDisabled = true; + spyOn((comp as any).browseService, 'getBrowseEntriesFor').and.returnValue(createSuccessfulRemoteDataObject$(null)); + }); + + it('should not call getBrowseEntriesFor on init', (done) => { + comp.ngOnInit(); + expect((comp as any).browseService.getBrowseEntriesFor).not.toHaveBeenCalled(); + comp.loading$.subscribe((res) => { + expect(res).toBeFalsy(); + done(); + }); + }); + }); + + describe('when rendered in CSR', () => { + beforeEach(() => { + comp.ssrRenderingDisabled = false; + spyOn((comp as any).browseService, 'getBrowseEntriesFor').and.returnValue(createSuccessfulRemoteDataObject$(new BrowseEntry())); + }); + + it('should call getBrowseEntriesFor on init', fakeAsync(() => { + comp.ngOnInit(); + tick(100); + expect((comp as any).browseService.getBrowseEntriesFor).toHaveBeenCalled(); + })); + }); }); export function toRemoteData(objects: any[]): Observable>> { diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts index fe407a2fb0..52cf3b9d7b 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts @@ -1,5 +1,5 @@ import { combineLatest as observableCombineLatest, Observable, Subscription, of as observableOf } from 'rxjs'; -import { Component, Inject, OnInit, OnDestroy } from '@angular/core'; +import { Component, Inject, OnInit, OnDestroy, Input, PLATFORM_ID } from '@angular/core'; import { RemoteData } from '../../core/data/remote-data'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; @@ -22,6 +22,8 @@ import { Collection } from '../../core/shared/collection.model'; import { Community } from '../../core/shared/community.model'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { isPlatformServer } from '@angular/common'; +import { environment } from '../../../environments/environment'; export const BBM_PAGINATION_ID = 'bbm'; @@ -37,7 +39,10 @@ export const BBM_PAGINATION_ID = 'bbm'; * 'dc.contributor.*' */ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { - + /** + * Defines whether to fetch search results during SSR execution + */ + @Input() renderOnServerSide = false; /** * The list of browse-entries to display */ @@ -126,6 +131,10 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { * Observable determining if the loading animation needs to be shown */ loading$ = observableOf(true); + /** + * Whether this component should be rendered or not in SSR + */ + ssrRenderingDisabled = false; public constructor(protected route: ActivatedRoute, protected browseService: BrowseService, @@ -134,6 +143,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { protected router: Router, @Inject(APP_CONFIG) public appConfig: AppConfig, public dsoNameService: DSONameService, + @Inject(PLATFORM_ID) public platformId: any, ) { this.fetchThumbnails = this.appConfig.browseBy.showThumbnails; @@ -142,11 +152,15 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { currentPage: 1, pageSize: this.appConfig.browseBy.pageSize, }); - } + this.ssrRenderingDisabled = !this.renderOnServerSide && !environment.universal.enableBrowseComponent && isPlatformServer(this.platformId); + } ngOnInit(): void { - + if (this.ssrRenderingDisabled) { + this.loading$ = observableOf(false); + return; + } const sortConfig = new SortOptions('default', SortDirection.ASC); this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig)); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts index e32c0ac430..1b8eb352a3 100644 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts +++ b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { Item } from '../../core/shared/item.model'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; @@ -22,6 +22,7 @@ import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { APP_CONFIG } from '../../../config/app-config.interface'; import { environment } from '../../../environments/environment'; +import { BrowseEntry } from '../../core/shared/browse-entry.model'; describe('BrowseByTitlePageComponent', () => { @@ -63,7 +64,8 @@ describe('BrowseByTitlePageComponent', () => { const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { params: observableOf({}), - data: observableOf({ metadata: 'title' }) + queryParams: observableOf({}), + data: observableOf({ metadata: 'title' }), }); const paginationService = new PaginationServiceStub(); @@ -97,4 +99,35 @@ describe('BrowseByTitlePageComponent', () => { expect(result.payload.page).toEqual(mockItems); }); }); + + describe('when rendered in SSR', () => { + beforeEach(() => { + comp.platformId = 'server'; + spyOn((comp as any).browseService, 'getBrowseItemsFor'); + fixture.detectChanges(); + }); + + it('should not call getBrowseItemsFor on init', (done) => { + comp.ngOnInit(); + expect((comp as any).browseService.getBrowseItemsFor).not.toHaveBeenCalled(); + comp.loading$.subscribe((res) => { + expect(res).toBeFalsy(); + done(); + }); + }); + }); + + describe('when rendered in CSR', () => { + beforeEach(() => { + comp.platformId = 'browser'; + fixture.detectChanges(); + spyOn((comp as any).browseService, 'getBrowseItemsFor').and.returnValue(createSuccessfulRemoteDataObject$(new BrowseEntry())); + }); + + it('should call getBrowseItemsFor on init', fakeAsync(() => { + comp.ngOnInit(); + tick(100); + expect((comp as any).browseService.getBrowseItemsFor).toHaveBeenCalled(); + })); + }); }); diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts index 11dc2a2a6a..518fdf8c15 100644 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts +++ b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts @@ -1,5 +1,5 @@ import { combineLatest as observableCombineLatest } from 'rxjs'; -import { Component, Inject } from '@angular/core'; +import { Component, Inject, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { hasValue } from '../../shared/empty.util'; import { @@ -11,9 +11,12 @@ import { BrowseService } from '../../core/browse/browse.service'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginationService } from '../../core/pagination/pagination.service'; import { map, take } from 'rxjs/operators'; +import { of as observableOf } from 'rxjs'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { isPlatformServer } from '@angular/common'; +import { environment } from '../../../environments/environment'; @Component({ selector: 'ds-browse-by-title-page', @@ -32,11 +35,16 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { protected router: Router, @Inject(APP_CONFIG) public appConfig: AppConfig, public dsoNameService: DSONameService, + @Inject(PLATFORM_ID) public platformId: any, ) { - super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService); + super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService, platformId); } ngOnInit(): void { + if (!this.renderOnServerSide && !environment.universal.enableBrowseComponent && isPlatformServer(this.platformId)) { + this.loading$ = observableOf(false); + return; + } const sortConfig = new SortOptions('dc.title', SortDirection.ASC); this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig); this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig); diff --git a/src/app/search-page/configuration-search-page.component.ts b/src/app/search-page/configuration-search-page.component.ts index 9196dda025..13e4709ca0 100644 --- a/src/app/search-page/configuration-search-page.component.ts +++ b/src/app/search-page/configuration-search-page.component.ts @@ -1,7 +1,7 @@ import { HostWindowService } from '../shared/host-window.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { SearchComponent } from '../shared/search/search.component'; -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, PLATFORM_ID } from '@angular/core'; import { pushInOut } from '../shared/animations/push'; import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; @@ -35,7 +35,8 @@ export class ConfigurationSearchPageComponent extends SearchComponent { protected routeService: RouteService, protected router: Router, @Inject(APP_CONFIG) protected appConfig: AppConfig, + @Inject(PLATFORM_ID) public platformId: any, ) { - super(service, sidebarService, windowService, searchConfigService, routeService, router, appConfig); + super(service, sidebarService, windowService, searchConfigService, routeService, router, appConfig, platformId); } } diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-filter.component.ts index 67e8906bb5..a440652fe5 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, Input, OnInit } from '@angular/core'; +import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; @@ -43,6 +43,8 @@ export class SearchFilterComponent implements OnInit { */ @Input() scope: string; + @Output() isVisibilityComputed = new EventEmitter(); + /** * True when the filter is 100% collapsed in the UI */ @@ -91,7 +93,9 @@ export class SearchFilterComponent implements OnInit { */ ngOnInit() { this.selectedValues$ = this.getSelectedValues(); - this.active$ = this.isActive(); + this.active$ = this.isActive().pipe( + startWith(true) + ); this.collapsed$ = this.isCollapsed(); this.initializeFilter(); this.selectedValues$.pipe(take(1)).subscribe((selectedValues) => { @@ -99,6 +103,9 @@ export class SearchFilterComponent implements OnInit { this.filterService.expand(this.filter.name); } }); + this.isActive().pipe(take(1)).subscribe(() => { + this.isVisibilityComputed.emit(true); + }); } /** @@ -187,7 +194,7 @@ export class SearchFilterComponent implements OnInit { } )); } - }), - startWith(true)); + }) + ); } } diff --git a/src/app/shared/search/search-filters/search-filters.component.html b/src/app/shared/search/search-filters/search-filters.component.html index c006d80c44..e1e54c683d 100644 --- a/src/app/shared/search/search-filters/search-filters.component.html +++ b/src/app/shared/search/search-filters/search-filters.component.html @@ -1,7 +1,13 @@

{{"search.filters.head" | translate}}

-
- +
+
+ + + + + {{"search.filters.reset" | translate}} diff --git a/src/app/shared/search/search-filters/search-filters.component.scss b/src/app/shared/search/search-filters/search-filters.component.scss index b5b2816e89..6170b9281c 100644 --- a/src/app/shared/search/search-filters/search-filters.component.scss +++ b/src/app/shared/search/search-filters/search-filters.component.scss @@ -1,2 +1,12 @@ @import '../../../../styles/variables'; -@import '../../../../styles/mixins'; \ No newline at end of file +@import '../../../../styles/mixins'; + +:host ::ng-deep { + ngx-skeleton-loader .skeleton-loader { + height: var(--ds-filters-skeleton-height); + margin-bottom: var(--ds-filters-skeleton-spacing); + padding: var(--ds-filters-skeleton-spacing); + background-color: var(--bs-light); + box-shadow: none; + } +} diff --git a/src/app/shared/search/search-filters/search-filters.component.spec.ts b/src/app/shared/search/search-filters/search-filters.component.spec.ts index 522459b603..864d7b583e 100644 --- a/src/app/shared/search/search-filters/search-filters.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filters.component.spec.ts @@ -9,6 +9,8 @@ import { SearchFiltersComponent } from './search-filters.component'; import { SearchService } from '../../../core/shared/search/search.service'; import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub'; +import { APP_CONFIG } from '../../../../config/app-config.interface'; +import { environment } from '../../../../environments/environment'; describe('SearchFiltersComponent', () => { let comp: SearchFiltersComponent; @@ -37,7 +39,7 @@ describe('SearchFiltersComponent', () => { { provide: SearchService, useValue: searchServiceStub }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: SearchFilterService, useValue: searchFiltersStub }, - + { provide: APP_CONFIG, useValue: environment }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(SearchFiltersComponent, { diff --git a/src/app/shared/search/search-filters/search-filters.component.ts b/src/app/shared/search/search-filters/search-filters.component.ts index 766939226d..b491f21177 100644 --- a/src/app/shared/search/search-filters/search-filters.component.ts +++ b/src/app/shared/search/search-filters/search-filters.component.ts @@ -2,7 +2,7 @@ import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { SearchService } from '../../../core/shared/search/search.service'; import { RemoteData } from '../../../core/data/remote-data'; @@ -12,6 +12,7 @@ import { SearchFilterService } from '../../../core/shared/search/search-filter.s import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { currentPath } from '../../utils/route.utils'; import { hasValue } from '../../empty.util'; +import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; @Component({ selector: 'ds-search-filters', @@ -60,7 +61,13 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { */ searchLink: string; + /** + * Filters for which visibility has been computed + */ + filtersWithComputedVisibility = 0; + subs = []; + defaultFilterCount: number; /** * Initialize instance variables @@ -68,19 +75,26 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { * @param {SearchFilterService} filterService * @param {Router} router * @param {SearchConfigurationService} searchConfigService + * @param appConfig */ constructor( private searchService: SearchService, private filterService: SearchFilterService, private router: Router, - @Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) { + @Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + ) { + this.defaultFilterCount = this.appConfig.search.filterPlaceholdersCount ?? 5; } ngOnInit(): void { - this.clearParams = this.searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => { - Object.keys(filters).forEach((f) => filters[f] = null); - return filters; - })); + this.clearParams = this.searchConfigService.getCurrentFrontendFilters().pipe( + tap(() => this.filtersWithComputedVisibility = 0), + map((filters) => { + Object.keys(filters).forEach((f) => filters[f] = null); + return filters; + }) + ); this.searchLink = this.getSearchLink(); } @@ -108,4 +122,10 @@ export class SearchFiltersComponent implements OnInit, OnDestroy { } }); } + + countFiltersWithComputedVisibility(computed: boolean) { + if (computed) { + this.filtersWithComputedVisibility += 1; + } + } } diff --git a/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.html b/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.html new file mode 100644 index 0000000000..c318eea2e1 --- /dev/null +++ b/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.html @@ -0,0 +1,38 @@ +
+
+ +
+
+ + + +
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ + +
+ +
+
+ +
+
+
+
+
+ diff --git a/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.scss b/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.scss new file mode 100644 index 0000000000..9345f1ab43 --- /dev/null +++ b/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.scss @@ -0,0 +1,56 @@ +:host ::ng-deep { + --ds-wrapper-grid-spacing: calc(var(--bs-spacer) / 2); + + .info-skeleton, .badge-skeleton, .text-skeleton{ + ngx-skeleton-loader .skeleton-loader { + height: var(--ds-search-skeleton-text-height); + } + } + + .badge-skeleton, .info-skeleton { + ngx-skeleton-loader .skeleton-loader { + width: var(--ds-search-skeleton-badge-width); + } + } + + .info-skeleton { + ngx-skeleton-loader .skeleton-loader { + width: var(--ds-search-skeleton-info-width); + } + } + + .thumbnail-skeleton { + max-width: var(--ds-thumbnail-max-width); + height: 100%; + + ngx-skeleton-loader .skeleton-loader { + margin-right: var(--ds-search-skeleton-thumbnail-margin); + border-radius: 0; + height: 100%; + } + } + + .card-skeleton { + ngx-skeleton-loader .skeleton-loader { + height: var(--ds-search-skeleton-card-height); + } + } + + ngx-skeleton-loader .skeleton-loader { + background-color: var(--bs-light); + box-shadow: none; + } + + .card-columns { + margin-left: calc(-1 * var(--ds-wrapper-grid-spacing)); + margin-right: calc(-1 * var(--ds-wrapper-grid-spacing)); + column-gap: 0; + + .card-column { + padding-left: var(--ds-wrapper-grid-spacing); + padding-right: var(--ds-wrapper-grid-spacing); + } + } +} + + diff --git a/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.spec.ts b/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.spec.ts new file mode 100644 index 0000000000..86277181fe --- /dev/null +++ b/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.spec.ts @@ -0,0 +1,33 @@ +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +import { SearchService } from '../../../../core/shared/search/search.service'; +import { SearchServiceStub } from '../../../testing/search-service.stub'; +import { SearchResultsSkeletonComponent } from './search-results-skeleton.component'; + +describe('SearchResultsSkeletonComponent', () => { + let component: SearchResultsSkeletonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NgxSkeletonLoaderModule], + declarations: [SearchResultsSkeletonComponent], + providers: [ + { provide: SearchService, useValue: new SearchServiceStub() }, + ], + }) + .compileComponents(); + + fixture = TestBed.createComponent(SearchResultsSkeletonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.ts b/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.ts new file mode 100644 index 0000000000..179b26f1fd --- /dev/null +++ b/src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.ts @@ -0,0 +1,59 @@ +import { + Component, + Input, + OnInit, +} from '@angular/core'; +import { Observable } from 'rxjs'; + +import { SearchService } from '../../../../core/shared/search/search.service'; +import { ViewMode } from '../../../../core/shared/view-mode.model'; +import { hasValue } from '../../../empty.util'; + +@Component({ + selector: 'ds-search-results-skeleton', + templateUrl: './search-results-skeleton.component.html', + styleUrls: ['./search-results-skeleton.component.scss'], +}) +/** + * Component to show placeholders for search results while loading, to give a loading feedback to the user without layout shifting. + */ +export class SearchResultsSkeletonComponent implements OnInit { + /** + * Whether the search result contains thumbnail + */ + @Input() + showThumbnails: boolean; + /** + * The number of search result loaded in the current page + */ + @Input() + numberOfResults = 0; + /** + * How many placeholder are displayed for the search result text + */ + @Input() + textLineCount = 2; + /** + * The view mode of the search page + */ + public viewMode$: Observable; + /** + * Array built from numberOfResults to count and iterate based on index + */ + public loadingResults: number[]; + + protected readonly ViewMode = ViewMode; + + constructor(private searchService: SearchService) { + this.viewMode$ = this.searchService.getViewMode(); + } + + ngOnInit() { + this.loadingResults = Array.from({ length: this.numberOfResults }, (_, i) => i + 1); + + if (!hasValue(this.showThumbnails)) { + // this is needed as the default value of show thumbnails is true but set in lower levels of the DOM. + this.showThumbnails = true; + } + } +} diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index 1f1e58ea10..a2bc91195c 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -19,7 +19,13 @@ (selectObject)="selectObject.emit($event)">
- + + +
diff --git a/src/app/shared/search/search-results/search-results.component.scss b/src/app/shared/search/search-results/search-results.component.scss new file mode 100644 index 0000000000..6e369c729b --- /dev/null +++ b/src/app/shared/search/search-results/search-results.component.scss @@ -0,0 +1,17 @@ +:host ::ng-deep { + .filter-badge-skeleton { + ngx-skeleton-loader .skeleton-loader { + background-color: var(--bs-light); + box-shadow: none; + width: var(--ds-search-skeleton-filter-badge-width); + height: var(--ds-search-skeleton-badge-height); + margin-bottom: 0; + margin-right: calc(var(--bs-spacer) / 4); + } + } + + .filters-badge-skeleton-container { + display: flex; + max-height: var(--ds-search-skeleton-badge-height); + } +} diff --git a/src/app/shared/search/search-results/search-results.component.spec.ts b/src/app/shared/search/search-results/search-results.component.spec.ts index 4cc4f84f65..1ef596a4ed 100644 --- a/src/app/shared/search/search-results/search-results.component.spec.ts +++ b/src/app/shared/search/search-results/search-results.component.spec.ts @@ -7,6 +7,11 @@ import { TranslateModule } from '@ngx-translate/core'; import { SearchResultsComponent } from './search-results.component'; import { QueryParamsDirectiveStub } from '../../testing/query-params-directive.stub'; import { createFailedRemoteDataObject } from '../../remote-data.utils'; +import { SearchResultsSkeletonComponent } from './search-results-skeleton/search-results-skeleton.component'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { SearchServiceStub } from '../../testing/search-service.stub'; describe('SearchResultsComponent', () => { let comp: SearchResultsComponent; @@ -19,7 +24,13 @@ describe('SearchResultsComponent', () => { imports: [TranslateModule.forRoot(), NoopAnimationsModule], declarations: [ SearchResultsComponent, - QueryParamsDirectiveStub], + SearchResultsSkeletonComponent, + QueryParamsDirectiveStub + ], + providers: [ + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, + { provide: SearchService, useValue: new SearchServiceStub() }, + ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -63,7 +74,7 @@ describe('SearchResultsComponent', () => { it('should display link with new search where query is quoted if search return a error 400', () => { (comp as any).searchResults = createFailedRemoteDataObject('Error', 400); - (comp as any).searchConfig = { query: 'foobar' }; + (comp as any).searchConfig = { query: 'foobar', pagination: { pageSize: 10 } }; fixture.detectChanges(); const linkDes = fixture.debugElement.queryAll(By.directive(QueryParamsDirectiveStub)); diff --git a/src/app/shared/search/search-results/search-results.component.ts b/src/app/shared/search/search-results/search-results.component.ts index 15d2cc0f00..9050ce9cd0 100644 --- a/src/app/shared/search/search-results/search-results.component.ts +++ b/src/app/shared/search/search-results/search-results.component.ts @@ -11,6 +11,10 @@ import { CollectionElementLinkType } from '../../object-collection/collection-el import { ViewMode } from '../../../core/shared/view-mode.model'; import { Context } from '../../../core/shared/context.model'; import { PaginatedSearchOptions } from '../models/paginated-search-options.model'; +import { SearchFilter } from '../models/search-filter.model'; +import { Observable } from 'rxjs'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { SearchService } from '../../../core/shared/search/search.service'; export interface SelectionConfig { repeatable: boolean; @@ -19,6 +23,7 @@ export interface SelectionConfig { @Component({ selector: 'ds-search-results', + styleUrls: ['./search-results.component.scss'], templateUrl: './search-results.component.html', animations: [ fadeIn, @@ -32,6 +37,8 @@ export interface SelectionConfig { export class SearchResultsComponent { hasNoValue = hasNoValue; + filters$: Observable; + /** * The link type of the listed search results */ @@ -104,6 +111,13 @@ export class SearchResultsComponent { @Output() selectObject: EventEmitter = new EventEmitter(); + constructor( + protected searchConfigService: SearchConfigurationService, + protected searchService: SearchService, + ) { + this.filters$ = this.searchConfigService.getCurrentFilters(); + } + /** * Check if search results are loading */ diff --git a/src/app/shared/search/search.component.spec.ts b/src/app/shared/search/search.component.spec.ts index 8ffd832009..05d4fc6b85 100644 --- a/src/app/shared/search/search.component.spec.ts +++ b/src/app/shared/search/search.component.spec.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; @@ -216,6 +216,7 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar useValue: searchConfigurationServiceStub }, { provide: APP_CONFIG, useValue: environment }, + { provide: PLATFORM_ID, useValue: 'browser' }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(compType, { @@ -374,5 +375,34 @@ describe('SearchComponent', () => { expect(result).toBeNull(); }); }); + + describe('when rendered in SSR', () => { + beforeEach(() => { + comp.platformId = 'server'; + }); + + it('should not call search method on init', (done) => { + comp.ngOnInit(); + //Check that the first method from which the search depend upon is not being called + expect(searchConfigurationServiceStub.getCurrentConfiguration).not.toHaveBeenCalled(); + comp.initialized$.subscribe((res) => { + expect(res).toBeTruthy(); + done(); + }); + }); + }); + + describe('when rendered in CSR', () => { + beforeEach(() => { + comp.platformId = 'browser'; + }); + + it('should call search method on init', fakeAsync(() => { + comp.ngOnInit(); + tick(100); + //Check that the last method from which the search depend upon is being called + expect(searchServiceStub.search).toHaveBeenCalled(); + })); + }); }); }); diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts index 5a848c9786..527d2d2719 100644 --- a/src/app/shared/search/search.component.ts +++ b/src/app/shared/search/search.component.ts @@ -1,4 +1,14 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Output, OnDestroy } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Inject, + Input, + OnInit, + Output, + OnDestroy, + PLATFORM_ID +} from '@angular/core'; import { NavigationStart, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; @@ -38,6 +48,8 @@ import { ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths'; import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths'; import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface'; +import { isPlatformServer } from '@angular/common'; +import { environment } from 'src/environments/environment'; @Component({ selector: 'ds-search', @@ -176,6 +188,11 @@ export class SearchComponent implements OnDestroy, OnInit { */ @Input() scope: string; + /** + * Defines whether to fetch search results during SSR execution + */ + @Input() renderOnServerSide = false; + /** * The current configuration used during the search */ @@ -285,6 +302,7 @@ export class SearchComponent implements OnDestroy, OnInit { protected routeService: RouteService, protected router: Router, @Inject(APP_CONFIG) protected appConfig: AppConfig, + @Inject(PLATFORM_ID) public platformId: any, ) { this.isXsOrSm$ = this.windowService.isXsOrSm(); } @@ -297,6 +315,14 @@ export class SearchComponent implements OnDestroy, OnInit { * If something changes, update the list of scopes for the dropdown */ ngOnInit(): void { + if (!this.renderOnServerSide && !environment.universal.enableSearchComponent && isPlatformServer(this.platformId)) { + this.subs.push(this.getSearchOptions().pipe(distinctUntilChanged()).subscribe((options) => { + this.searchOptions$.next(options); + })); + this.initialized$.next(true); + return; + } + if (this.useUniquePageId) { // Create an unique pagination id related to the instance of the SearchComponent this.paginationId = uniqueId(this.paginationId); diff --git a/src/app/shared/search/search.module.ts b/src/app/shared/search/search.module.ts index 69500999a8..e2075a5f56 100644 --- a/src/app/shared/search/search.module.ts +++ b/src/app/shared/search/search.module.ts @@ -34,6 +34,10 @@ import { ThemedSearchSettingsComponent } from './search-settings/themed-search-s import { NouisliderModule } from 'ng2-nouislider'; import { ThemedSearchFiltersComponent } from './search-filters/themed-search-filters.component'; import { ThemedSearchSidebarComponent } from './search-sidebar/themed-search-sidebar.component'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { + SearchResultsSkeletonComponent +} from './search-results/search-results-skeleton/search-results-skeleton.component'; const COMPONENTS = [ SearchComponent, @@ -62,6 +66,7 @@ const COMPONENTS = [ ThemedSearchSettingsComponent, ThemedSearchFiltersComponent, ThemedSearchSidebarComponent, + SearchResultsSkeletonComponent ]; const ENTRY_COMPONENTS = [ @@ -74,6 +79,7 @@ const ENTRY_COMPONENTS = [ SearchFacetSelectedOptionComponent, SearchFacetRangeOptionComponent, SearchAuthorityFilterComponent, + SearchResultsSkeletonComponent ]; /** @@ -93,11 +99,12 @@ export const MODELS = [ imports: [ CommonModule, TranslateModule.forChild({ - missingTranslationHandler: { provide: MissingTranslationHandler, useClass: MissingTranslationHelper }, + missingTranslationHandler: {provide: MissingTranslationHandler, useClass: MissingTranslationHelper}, useDefaultLang: true }), SharedModule.withEntryComponents(), NouisliderModule, + NgxSkeletonLoaderModule, ], exports: [ ...COMPONENTS diff --git a/src/app/shared/testing/search-configuration-service.stub.ts b/src/app/shared/testing/search-configuration-service.stub.ts index 78b358f0d4..ef72e40041 100644 --- a/src/app/shared/testing/search-configuration-service.stub.ts +++ b/src/app/shared/testing/search-configuration-service.stub.ts @@ -13,6 +13,10 @@ export class SearchConfigurationServiceStub { return observableOf([]); } + getCurrentFilters() { + return observableOf([]); + } + getCurrentScope(a) { return observableOf('test-id'); } diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index a2bf0cb876..af78d4ab88 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -23,6 +23,7 @@ import { MarkdownConfig } from './markdown-config.interface'; import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { DiscoverySortConfig } from './discovery-sort.config'; import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; +import { SearchConfig } from './search-page-config.interface'; interface AppConfig extends Config { ui: UIServerConfig; @@ -50,6 +51,7 @@ interface AppConfig extends Config { vocabularies: FilterVocabularyConfig[]; comcolSelectionSort: DiscoverySortConfig; liveRegion: LiveRegionConfig; + search: SearchConfig } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index a3a490538c..de4f3bd56e 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -23,6 +23,7 @@ import { MarkdownConfig } from './markdown-config.interface'; import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { DiscoverySortConfig } from './discovery-sort.config'; import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; +import { SearchConfig } from './search-page-config.interface'; export class DefaultAppConfig implements AppConfig { production = false; @@ -442,4 +443,8 @@ export class DefaultAppConfig implements AppConfig { messageTimeOutDurationMs: 30000, isVisible: false, }; + + search: SearchConfig = { + filterPlaceholdersCount: 5 + }; } diff --git a/src/config/search-page-config.interface.ts b/src/config/search-page-config.interface.ts new file mode 100644 index 0000000000..410876cde2 --- /dev/null +++ b/src/config/search-page-config.interface.ts @@ -0,0 +1,12 @@ +import { Config } from './config.interface'; + +export interface SearchConfig extends Config { + /** + * Number used to render n UI elements called loading skeletons that act as placeholders. + * These elements indicate that some content will be loaded in their stead. + * Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count. + * For instance if we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved. + */ + filterPlaceholdersCount?: number; + +} diff --git a/src/config/universal-config.interface.ts b/src/config/universal-config.interface.ts index e54168823f..e3f7f399a9 100644 --- a/src/config/universal-config.interface.ts +++ b/src/config/universal-config.interface.ts @@ -18,4 +18,13 @@ export interface UniversalConfig extends Config { * Paths to enable SSR for. Defaults to the home page and paths in the sitemap. */ paths: Array; + /** + * Whether to enable rendering of search component on SSR + */ + enableSearchComponent: boolean; + + /** + * Whether to enable rendering of browse component on SSR + */ + enableBrowseComponent: boolean; } diff --git a/src/environments/environment.production.ts b/src/environments/environment.production.ts index 46a93519df..c3cb74651b 100644 --- a/src/environments/environment.production.ts +++ b/src/environments/environment.production.ts @@ -10,5 +10,7 @@ export const environment: Partial = { time: false, inlineCriticalCss: false, paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ], - } + enableSearchComponent: false, + enableBrowseComponent: false, + }, }; diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index e872285f61..e6ffe85df6 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -13,6 +13,8 @@ export const environment: BuildConfig = { time: false, inlineCriticalCss: false, paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ], + enableSearchComponent: false, + enableBrowseComponent: false, }, // Angular Universal server settings. @@ -321,4 +323,8 @@ export const environment: BuildConfig = { messageTimeOutDurationMs: 30000, isVisible: false, }, + + search: { + filterPlaceholdersCount: 5 + } }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 25af371e47..419238f264 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -15,7 +15,9 @@ export const environment: Partial = { time: false, inlineCriticalCss: false, paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ], - } + enableSearchComponent: false, + enableBrowseComponent: false, + }, }; /* diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index aa67acac1c..3c3244549e 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -138,4 +138,16 @@ --green1: #1FB300; // This variable represents the success color for the Klaro cookie banner --button-text-color-cookie: #333; // This variable represents the text color for buttons in the Klaro cookie banner --very-dark-cyan: #215E50; // This variable represents the background color of the save cookies button + + --ds-search-skeleton-text-height: 20px; + --ds-search-skeleton-badge-height: 18px; + --ds-search-skeleton-thumbnail-margin: 1em; + --ds-search-skeleton-text-line-count: 2; + --ds-search-skeleton-badge-width: 75px; + --ds-search-skeleton-filter-badge-width: 200px; + --ds-search-skeleton-info-width: 200px; + --ds-search-skeleton-card-height: 435px; + + --ds-filters-skeleton-height: 40px; + --ds-filters-skeleton-spacing: 12px; } diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts index 73400e7880..546d2dccbf 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -159,6 +159,8 @@ import { RequestCopyModule } from 'src/app/request-copy/request-copy.module'; import {UserMenuComponent} from './app/shared/auth-nav-menu/user-menu/user-menu.component'; import { BrowseByComponent } from './app/shared/browse-by/browse-by.component'; import { RegisterEmailFormComponent } from './app/register-email-form/register-email-form.component'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + const DECLARATIONS = [ FileSectionComponent, @@ -305,6 +307,7 @@ const DECLARATIONS = [ NgxGalleryModule, FormModule, RequestCopyModule, + NgxSkeletonLoaderModule ], declarations: DECLARATIONS, exports: [ diff --git a/src/themes/dspace/styles/_theme_css_variable_overrides.scss b/src/themes/dspace/styles/_theme_css_variable_overrides.scss index 8b95cd0033..46d73997eb 100644 --- a/src/themes/dspace/styles/_theme_css_variable_overrides.scss +++ b/src/themes/dspace/styles/_theme_css_variable_overrides.scss @@ -18,6 +18,7 @@ /* set the next two properties as `--ds-header-navbar-border-bottom-*` in order to keep the bottom border of the header when navbar is expanded */ + --ds-expandable-navbar-border-top-color: #{$white}; --ds-expandable-navbar-border-top-height: 0; --ds-expandable-navbar-padding-top: 0; diff --git a/yarn.lock b/yarn.lock index 58d8f59fbc..86a07c555a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8415,6 +8415,14 @@ ngx-pagination@6.0.3: dependencies: tslib "^2.3.0" +ngx-skeleton-loader@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ngx-skeleton-loader/-/ngx-skeleton-loader-7.0.0.tgz#3b1325025a7208a20f3a0fdba6e578532a09cfcd" + integrity sha512-myc6GNcNhyksZrimIFkCxeihi0kQ8JhQVZiGbtiIv4gYrnnRk5nXbs3kYitK8E8OstHG+jlsmRofqGBxuIsYTA== + dependencies: + perf-marks "^1.13.4" + tslib "^2.0.0" + ngx-sortablejs@^11.1.0: version "11.1.0" resolved "https://registry.npmjs.org/ngx-sortablejs/-/ngx-sortablejs-11.1.0.tgz" @@ -9060,6 +9068,13 @@ pend@~1.2.0: resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== +perf-marks@^1.13.4: + version "1.14.2" + resolved "https://registry.yarnpkg.com/perf-marks/-/perf-marks-1.14.2.tgz#7511c24239b9c2071717993a33ec3057f387b8c7" + integrity sha512-N0/bQcuTlETpgox/DsXS1voGjqaoamMoiyhncgeW3rSHy/qw8URVgmPRYfFDQns/+C6yFUHDbeSBGL7ixT6Y4A== + dependencies: + tslib "^2.1.0" + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"