From 17ecc592f3b43b4cb1f83655e735ff44340580b8 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro <152612515+FrancescoMolinaro@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:07:15 +0100 Subject: [PATCH] Exclude search and browse from Angular SSR (#3709) * [DURACOM-303] prevent possibly long-lasting search and browse calls in SSR * [DURACOM-303] implement skeleton component for search results * [DURACOM-303] add skeleton loader for search results and filters * [DURACOM-303] minor restyle of skeleton for mobile * [DURACOM-303] fix lint and tests * [DURACOM-303] adapt tests * [DURACOM-303] restyle skeleton, add filter badge skeleton * [DURACOM-303] add loop for filters count * [DURACOM-303] add grid layout, make SSR enabling configurable, minor restyle of skeletons * [DURACOM-303] refactor param, add example of configuration * [DURACOM-303] rename variable, minor code refactor * [DURACOM-303] add override possibility with input * [DURACOM-303] fix SSR check on template and on components missing the environment config. Add descriptive comment for skeleton component. Fix JS error on SSR. * [DURACOM-303] refactor thumbnail's skeleton style --- config/config.example.yml | 14 ++++ package-lock.json | 14 ++++ package.json | 1 + .../browse-by-date.component.spec.ts | 34 +++++++++ .../browse-by-date.component.ts | 11 ++- .../browse-by-metadata.component.html | 2 +- .../browse-by-metadata.component.spec.ts | 37 +++++++++- .../browse-by-metadata.component.ts | 20 +++++- .../browse-by-title.component.spec.ts | 35 ++++++++++ .../browse-by-title.component.ts | 11 ++- .../configuration-search-page.component.ts | 4 +- .../search-filter/search-filter.component.ts | 30 ++++++-- .../search-filters.component.html | 34 ++++++--- .../search-filters.component.scss | 12 +++- .../search-filters.component.spec.ts | 3 + .../search-filters.component.ts | 28 ++++++-- .../search-results-skeleton.component.html | 38 ++++++++++ .../search-results-skeleton.component.scss | 56 +++++++++++++++ .../search-results-skeleton.component.spec.ts | 32 +++++++++ .../search-results-skeleton.component.ts | 70 +++++++++++++++++++ .../search-results.component.html | 20 +++++- .../search-results.component.scss | 17 +++++ .../search-results.component.spec.ts | 15 +++- .../search-results.component.ts | 38 ++++++++-- .../shared/search/search.component.spec.ts | 31 ++++++++ src/app/shared/search/search.component.ts | 18 +++++ .../search-configuration-service.stub.ts | 4 ++ src/config/default-app-config.ts | 1 + src/config/search-page-config.interface.ts | 8 ++- src/config/ssr-config.interface.ts | 10 +++ src/environments/environment.production.ts | 2 + src/environments/environment.test.ts | 2 + src/environments/environment.ts | 2 + src/styles/_custom_variables.scss | 12 ++++ .../search-filters.component.ts | 10 +-- .../search-results.component.ts | 12 ++-- src/themes/custom/lazy-theme.module.ts | 3 + .../styles/_theme_css_variable_overrides.scss | 1 + 38 files changed, 641 insertions(+), 51 deletions(-) create mode 100644 src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.html create mode 100644 src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.scss create mode 100644 src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.spec.ts create mode 100644 src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.ts create mode 100644 src/app/shared/search/search-results/search-results.component.scss diff --git a/config/config.example.yml b/config/config.example.yml index 3ae2c51caec..099bea2614c 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 @@ -450,6 +458,12 @@ search: enabled: false # List of filters to enable in "Advanced Search" dropdown filter: [ 'title', 'author', 'subject', 'entityType' ] + # + # 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 # Notify metrics diff --git a/package-lock.json b/package-lock.json index 904bcc5c740..48ef9fa63b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,7 @@ "ng2-nouislider": "^2.0.0", "ngx-infinite-scroll": "^16.0.0", "ngx-pagination": "6.0.3", + "ngx-skeleton-loader": "^9.0.0", "ngx-ui-switch": "^14.1.0", "nouislider": "^15.7.1", "orejime": "^2.3.1", @@ -16941,6 +16942,19 @@ "@angular/core": ">=13.0.0" } }, + "node_modules/ngx-skeleton-loader": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-9.0.0.tgz", + "integrity": "sha512-aO4/V6oGdZGNcTjasTg/fwzJJYl/ZmNKgCukOEQdUK3GSFOZtB/3GGULMJuZ939hk3Hzqh1OBiLfIM1SqTfhqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0" + } + }, "node_modules/ngx-ui-switch": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/ngx-ui-switch/-/ngx-ui-switch-14.1.0.tgz", diff --git a/package.json b/package.json index 482a400bcbf..e56780a76d6 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,7 @@ "ng2-nouislider": "^2.0.0", "ngx-infinite-scroll": "^16.0.0", "ngx-pagination": "6.0.3", + "ngx-skeleton-loader": "^9.0.0", "ngx-ui-switch": "^14.1.0", "nouislider": "^15.7.1", "orejime": "^2.3.1", diff --git a/src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts b/src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts index edd7cd951a4..fad573705c5 100644 --- a/src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts +++ b/src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts @@ -2,10 +2,13 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, NO_ERRORS_SCHEMA, + PLATFORM_ID, } from '@angular/core'; import { ComponentFixture, + fakeAsync, TestBed, + tick, waitForAsync, } from '@angular/core/testing'; import { @@ -26,6 +29,7 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search- import { SortDirection } from '../../core/cache/models/sort-options.model'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { PaginationService } from '../../core/pagination/pagination.service'; +import { BrowseEntry } from '../../core/shared/browse-entry.model'; import { Community } from '../../core/shared/community.model'; import { Item } from '../../core/shared/item.model'; import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component'; @@ -123,6 +127,7 @@ describe('BrowseByDateComponent', () => { { provide: ChangeDetectorRef, useValue: mockCdRef }, { provide: Store, useValue: {} }, { provide: APP_CONFIG, useValue: environment }, + { provide: PLATFORM_ID, useValue: 'browser' }, ], schemas: [NO_ERRORS_SCHEMA], }) @@ -172,4 +177,33 @@ describe('BrowseByDateComponent', () => { //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/browse-by-date.component.ts b/src/app/browse-by/browse-by-date/browse-by-date.component.ts index 11818ff5f15..5c14f2109c7 100644 --- a/src/app/browse-by/browse-by-date/browse-by-date.component.ts +++ b/src/app/browse-by/browse-by-date/browse-by-date.component.ts @@ -1,5 +1,6 @@ import { AsyncPipe, + isPlatformServer, NgIf, } from '@angular/common'; import { @@ -7,6 +8,7 @@ import { Component, Inject, OnInit, + PLATFORM_ID, } from '@angular/core'; import { ActivatedRoute, @@ -17,6 +19,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { combineLatest as observableCombineLatest, Observable, + of as observableOf, } from 'rxjs'; import { map, @@ -28,6 +31,7 @@ import { APP_CONFIG, AppConfig, } from '../../../config/app-config.interface'; +import { environment } from '../../../environments/environment'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { BrowseService } from '../../core/browse/browse.service'; import { @@ -99,11 +103,16 @@ export class BrowseByDateComponent extends BrowseByMetadataComponent implements @Inject(APP_CONFIG) public appConfig: AppConfig, public dsoNameService: DSONameService, protected cdRef: ChangeDetectorRef, + @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.ssr.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/browse-by-metadata.component.html b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html index 22e564ac27c..6642b724bb9 100644 --- a/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html +++ b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html @@ -1,4 +1,4 @@ -
+