mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
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
This commit is contained in:

committed by
Tim Donohue

parent
cb8a7cd402
commit
17ecc592f3
@@ -25,6 +25,14 @@ ssr:
|
|||||||
inlineCriticalCss: false
|
inlineCriticalCss: false
|
||||||
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
|
# 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/' ]
|
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
|
# The REST API server settings
|
||||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||||
@@ -450,6 +458,12 @@ search:
|
|||||||
enabled: false
|
enabled: false
|
||||||
# List of filters to enable in "Advanced Search" dropdown
|
# List of filters to enable in "Advanced Search" dropdown
|
||||||
filter: [ 'title', 'author', 'subject', 'entityType' ]
|
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
|
# Notify metrics
|
||||||
|
14
package-lock.json
generated
14
package-lock.json
generated
@@ -67,6 +67,7 @@
|
|||||||
"ng2-nouislider": "^2.0.0",
|
"ng2-nouislider": "^2.0.0",
|
||||||
"ngx-infinite-scroll": "^16.0.0",
|
"ngx-infinite-scroll": "^16.0.0",
|
||||||
"ngx-pagination": "6.0.3",
|
"ngx-pagination": "6.0.3",
|
||||||
|
"ngx-skeleton-loader": "^9.0.0",
|
||||||
"ngx-ui-switch": "^14.1.0",
|
"ngx-ui-switch": "^14.1.0",
|
||||||
"nouislider": "^15.7.1",
|
"nouislider": "^15.7.1",
|
||||||
"orejime": "^2.3.1",
|
"orejime": "^2.3.1",
|
||||||
@@ -16941,6 +16942,19 @@
|
|||||||
"@angular/core": ">=13.0.0"
|
"@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": {
|
"node_modules/ngx-ui-switch": {
|
||||||
"version": "14.1.0",
|
"version": "14.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ngx-ui-switch/-/ngx-ui-switch-14.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ngx-ui-switch/-/ngx-ui-switch-14.1.0.tgz",
|
||||||
|
@@ -154,6 +154,7 @@
|
|||||||
"ng2-nouislider": "^2.0.0",
|
"ng2-nouislider": "^2.0.0",
|
||||||
"ngx-infinite-scroll": "^16.0.0",
|
"ngx-infinite-scroll": "^16.0.0",
|
||||||
"ngx-pagination": "6.0.3",
|
"ngx-pagination": "6.0.3",
|
||||||
|
"ngx-skeleton-loader": "^9.0.0",
|
||||||
"ngx-ui-switch": "^14.1.0",
|
"ngx-ui-switch": "^14.1.0",
|
||||||
"nouislider": "^15.7.1",
|
"nouislider": "^15.7.1",
|
||||||
"orejime": "^2.3.1",
|
"orejime": "^2.3.1",
|
||||||
|
@@ -2,10 +2,13 @@ import { CommonModule } from '@angular/common';
|
|||||||
import {
|
import {
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
NO_ERRORS_SCHEMA,
|
NO_ERRORS_SCHEMA,
|
||||||
|
PLATFORM_ID,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
|
fakeAsync,
|
||||||
TestBed,
|
TestBed,
|
||||||
|
tick,
|
||||||
waitForAsync,
|
waitForAsync,
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +29,7 @@ import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-
|
|||||||
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
|
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
|
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
|
||||||
@@ -123,6 +127,7 @@ describe('BrowseByDateComponent', () => {
|
|||||||
{ provide: ChangeDetectorRef, useValue: mockCdRef },
|
{ provide: ChangeDetectorRef, useValue: mockCdRef },
|
||||||
{ provide: Store, useValue: {} },
|
{ provide: Store, useValue: {} },
|
||||||
{ provide: APP_CONFIG, useValue: environment },
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
})
|
})
|
||||||
@@ -172,4 +177,33 @@ describe('BrowseByDateComponent', () => {
|
|||||||
//expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
|
//expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
|
||||||
expect(comp.startsWithOptions[0]).toEqual(1960);
|
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();
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
isPlatformServer,
|
||||||
NgIf,
|
NgIf,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
@@ -7,6 +8,7 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
PLATFORM_ID,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
@@ -17,6 +19,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import {
|
import {
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
@@ -28,6 +31,7 @@ import {
|
|||||||
APP_CONFIG,
|
APP_CONFIG,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
} from '../../../config/app-config.interface';
|
} from '../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import {
|
import {
|
||||||
@@ -99,11 +103,16 @@ export class BrowseByDateComponent extends BrowseByMetadataComponent implements
|
|||||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
protected cdRef: ChangeDetectorRef,
|
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 {
|
ngOnInit(): void {
|
||||||
|
if (!this.renderOnServerSide && !environment.ssr.enableBrowseComponent && isPlatformServer(this.platformId)) {
|
||||||
|
this.loading$ = observableOf(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||||
this.startsWithType = StartsWithType.date;
|
this.startsWithType = StartsWithType.date;
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section" *ngIf="(!ssrRenderingDisabled)">
|
||||||
<div class="browse-by-metadata w-100">
|
<div class="browse-by-metadata w-100">
|
||||||
<ds-browse-by *ngIf="(loading$ | async) !== true" class="col-xs-12 w-100"
|
<ds-browse-by *ngIf="(loading$ | async) !== true" class="col-xs-12 w-100"
|
||||||
title="{{'browse.title' | translate:{
|
title="{{'browse.title' | translate:{
|
||||||
|
@@ -1,8 +1,13 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import {
|
||||||
|
NO_ERRORS_SCHEMA,
|
||||||
|
PLATFORM_ID,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
|
fakeAsync,
|
||||||
TestBed,
|
TestBed,
|
||||||
|
tick,
|
||||||
waitForAsync,
|
waitForAsync,
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
@@ -147,6 +152,7 @@ describe('BrowseByMetadataComponent', () => {
|
|||||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
{ provide: SelectableListService, useValue: {} },
|
{ provide: SelectableListService, useValue: {} },
|
||||||
{ provide: HostWindowService, useValue: {} },
|
{ provide: HostWindowService, useValue: {} },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
})
|
})
|
||||||
@@ -259,6 +265,35 @@ describe('BrowseByMetadataComponent', () => {
|
|||||||
expect(result.fetchThumbnail).toBeTrue();
|
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<RemoteData<PaginatedList<any>>> {
|
export function toRemoteData(objects: any[]): Observable<RemoteData<PaginatedList<any>>> {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
isPlatformServer,
|
||||||
NgIf,
|
NgIf,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
PLATFORM_ID,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
APP_CONFIG,
|
APP_CONFIG,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
} from '../../../config/app-config.interface';
|
} from '../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||||
@@ -114,6 +117,11 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Input() displayTitle = true;
|
@Input() displayTitle = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines whether to fetch search results during SSR execution
|
||||||
|
*/
|
||||||
|
@Input() renderOnServerSide: boolean;
|
||||||
|
|
||||||
scope$: BehaviorSubject<string> = new BehaviorSubject(undefined);
|
scope$: BehaviorSubject<string> = new BehaviorSubject(undefined);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -194,6 +202,10 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
* Observable determining if the loading animation needs to be shown
|
* Observable determining if the loading animation needs to be shown
|
||||||
*/
|
*/
|
||||||
loading$ = observableOf(true);
|
loading$ = observableOf(true);
|
||||||
|
/**
|
||||||
|
* Whether this component should be rendered or not in SSR
|
||||||
|
*/
|
||||||
|
ssrRenderingDisabled = false;
|
||||||
|
|
||||||
public constructor(protected route: ActivatedRoute,
|
public constructor(protected route: ActivatedRoute,
|
||||||
protected browseService: BrowseService,
|
protected browseService: BrowseService,
|
||||||
@@ -202,6 +214,7 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
protected router: Router,
|
protected router: Router,
|
||||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
|
@Inject(PLATFORM_ID) public platformId: any,
|
||||||
) {
|
) {
|
||||||
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
|
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||||
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||||
@@ -209,11 +222,15 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: this.appConfig.browseBy.pageSize,
|
pageSize: this.appConfig.browseBy.pageSize,
|
||||||
});
|
});
|
||||||
|
this.ssrRenderingDisabled = !this.renderOnServerSide && !environment.ssr.enableBrowseComponent && isPlatformServer(this.platformId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
if (this.ssrRenderingDisabled) {
|
||||||
|
this.loading$ = observableOf(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
const sortConfig = new SortOptions('default', SortDirection.ASC);
|
||||||
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig));
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
@@ -336,7 +353,6 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.paginationService.clearPagination(this.paginationConfig.id);
|
this.paginationService.clearPagination(this.paginationConfig.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -5,7 +5,9 @@ import {
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
|
fakeAsync,
|
||||||
TestBed,
|
TestBed,
|
||||||
|
tick,
|
||||||
waitForAsync,
|
waitForAsync,
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +25,7 @@ import { BrowseService } from '../../core/browse/browse.service';
|
|||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
|
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
|
import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component';
|
||||||
@@ -81,6 +84,7 @@ describe('BrowseByTitleComponent', () => {
|
|||||||
|
|
||||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
params: observableOf({}),
|
params: observableOf({}),
|
||||||
|
queryParams: observableOf({}),
|
||||||
data: observableOf({ metadata: 'title' }),
|
data: observableOf({ metadata: 'title' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,4 +131,35 @@ describe('BrowseByTitleComponent', () => {
|
|||||||
expect(result.payload.page).toEqual(mockItems);
|
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();
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
isPlatformServer,
|
||||||
NgIf,
|
NgIf,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
@@ -8,12 +9,16 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
import {
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
take,
|
take,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
import {
|
import {
|
||||||
SortDirection,
|
SortDirection,
|
||||||
SortOptions,
|
SortOptions,
|
||||||
@@ -59,6 +64,10 @@ import {
|
|||||||
export class BrowseByTitleComponent extends BrowseByMetadataComponent implements OnInit {
|
export class BrowseByTitleComponent extends BrowseByMetadataComponent implements OnInit {
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
if (!this.renderOnServerSide && !environment.ssr.enableBrowseComponent && isPlatformServer(this.platformId)) {
|
||||||
|
this.loading$ = observableOf(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
|
||||||
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
|
||||||
|
@@ -7,6 +7,7 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
|
PLATFORM_ID,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -57,7 +58,8 @@ export class ConfigurationSearchPageComponent extends SearchComponent {
|
|||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
@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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,13 @@ import {
|
|||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
EventEmitter,
|
||||||
Inject,
|
Inject,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
Output,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
@@ -78,6 +80,8 @@ export class SearchFilterComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Input() scope: string;
|
@Input() scope: string;
|
||||||
|
|
||||||
|
@Output() isVisibilityComputed = new EventEmitter<boolean>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when the filter is 100% collapsed in the UI
|
* True when the filter is 100% collapsed in the UI
|
||||||
*/
|
*/
|
||||||
@@ -136,11 +140,16 @@ export class SearchFilterComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.active$ = this.isActive();
|
this.active$ = this.isActive();
|
||||||
this.collapsed$ = this.isCollapsed();
|
this.collapsed$ = this.isCollapsed();
|
||||||
this.initializeFilter();
|
this.initializeFilter();
|
||||||
this.subs.push(this.appliedFilters$.pipe(take(1)).subscribe((selectedValues: AppliedFilter[]) => {
|
this.subs.push(
|
||||||
if (isNotEmpty(selectedValues)) {
|
this.appliedFilters$.subscribe((selectedValues: AppliedFilter[]) => {
|
||||||
this.filterService.expand(this.filter.name);
|
if (isNotEmpty(selectedValues)) {
|
||||||
}
|
this.filterService.expand(this.filter.name);
|
||||||
}));
|
}
|
||||||
|
}),
|
||||||
|
this.getIsActive().pipe(take(1)).subscribe(() => {
|
||||||
|
this.isVisibilityComputed.emit(true);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
@@ -223,6 +232,16 @@ export class SearchFilterComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
||||||
*/
|
*/
|
||||||
isActive(): Observable<boolean> {
|
isActive(): Observable<boolean> {
|
||||||
|
return this.getIsActive().pipe(
|
||||||
|
startWith(true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current filter visibility
|
||||||
|
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
|
||||||
|
*/
|
||||||
|
private getIsActive(): Observable<boolean> {
|
||||||
return combineLatest([
|
return combineLatest([
|
||||||
this.appliedFilters$,
|
this.appliedFilters$,
|
||||||
this.searchConfigService.searchOptions,
|
this.searchConfigService.searchOptions,
|
||||||
@@ -243,7 +262,6 @@ export class SearchFilterComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
startWith(true),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,24 @@
|
|||||||
<h3 *ngIf="inPlaceSearch">{{filterLabel+'.filters.head' | translate}}</h3>
|
@if (inPlaceSearch) {
|
||||||
<h2 *ngIf="!inPlaceSearch">{{filterLabel+'.filters.head' | translate}}</h2>
|
<h3>{{filterLabel+'.filters.head' | translate}}</h3>
|
||||||
<div *ngIf="(filters | async)?.hasSucceeded">
|
} @else {
|
||||||
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
|
<h2>{{filterLabel+'.filters.head' | translate}}</h2>
|
||||||
<ds-search-filter [scope]="currentScope" [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter>
|
}
|
||||||
</div>
|
|
||||||
</div>
|
@if ((filters | async)?.hasSucceeded) {
|
||||||
<button *ngIf="inPlaceSearch" class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" (click)="minimizeFilters()" queryParamsHandling="merge" role="button">
|
<div [class.visually-hidden]="filtersWithComputedVisibility !== (filters | async)?.payload?.length">
|
||||||
<i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}
|
@for (filter of (filters | async)?.payload; track filter.name) {
|
||||||
</button>
|
<ds-search-filter (isVisibilityComputed)="countFiltersWithComputedVisibility($event)" [scope]="currentScope" [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if(filtersWithComputedVisibility !== (filters | async)?.payload?.length) {
|
||||||
|
<ngx-skeleton-loader [count]="defaultFilterCount"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (inPlaceSearch) {
|
||||||
|
<button class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" (click)="minimizeFilters()" queryParamsHandling="merge" role="button">
|
||||||
|
<i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -1,2 +1,12 @@
|
|||||||
@import '../../../../styles/variables';
|
@import '../../../../styles/variables';
|
||||||
@import '../../../../styles/mixins';
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -11,6 +11,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { APP_CONFIG } from '../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service';
|
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service';
|
||||||
@@ -41,6 +43,7 @@ describe('SearchFiltersComponent', () => {
|
|||||||
{ provide: SearchService, useValue: searchService },
|
{ provide: SearchService, useValue: searchService },
|
||||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||||
{ provide: SearchFilterService, useValue: searchFilters },
|
{ provide: SearchFilterService, useValue: searchFilters },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).overrideComponent(SearchFiltersComponent, {
|
}).overrideComponent(SearchFiltersComponent, {
|
||||||
|
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { AsyncPipe } from '@angular/common';
|
||||||
AsyncPipe,
|
|
||||||
NgFor,
|
|
||||||
NgIf,
|
|
||||||
} from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
@@ -14,19 +10,23 @@ import {
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
Observable,
|
Observable,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import {
|
||||||
|
APP_CONFIG,
|
||||||
|
AppConfig,
|
||||||
|
} from '../../../../config/app-config.interface';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||||
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service';
|
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service';
|
||||||
import { currentPath } from '../../utils/route.utils';
|
import { currentPath } from '../../utils/route.utils';
|
||||||
import { AdvancedSearchComponent } from '../advanced-search/advanced-search.component';
|
|
||||||
import { AppliedFilter } from '../models/applied-filter.model';
|
import { AppliedFilter } from '../models/applied-filter.model';
|
||||||
import { SearchFilterConfig } from '../models/search-filter-config.model';
|
import { SearchFilterConfig } from '../models/search-filter-config.model';
|
||||||
import { SearchFilterComponent } from './search-filter/search-filter.component';
|
import { SearchFilterComponent } from './search-filter/search-filter.component';
|
||||||
@@ -36,7 +36,7 @@ import { SearchFilterComponent } from './search-filter/search-filter.component';
|
|||||||
styleUrls: ['./search-filters.component.scss'],
|
styleUrls: ['./search-filters.component.scss'],
|
||||||
templateUrl: './search-filters.component.html',
|
templateUrl: './search-filters.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, NgFor, SearchFilterComponent, RouterLink, AsyncPipe, TranslateModule, AdvancedSearchComponent],
|
imports: [SearchFilterComponent, RouterLink, AsyncPipe, TranslateModule, NgxSkeletonLoaderModule],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,15 +81,23 @@ export class SearchFiltersComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
searchLink: string;
|
searchLink: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters for which visibility has been computed
|
||||||
|
*/
|
||||||
|
filtersWithComputedVisibility = 0;
|
||||||
|
|
||||||
subs = [];
|
subs = [];
|
||||||
filterLabel = 'search';
|
filterLabel = 'search';
|
||||||
|
defaultFilterCount: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected searchService: SearchService,
|
protected searchService: SearchService,
|
||||||
protected searchFilterService: SearchFilterService,
|
protected searchFilterService: SearchFilterService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
@Inject(SEARCH_CONFIG_SERVICE) protected searchConfigService: SearchConfigurationService,
|
@Inject(SEARCH_CONFIG_SERVICE) protected searchConfigService: SearchConfigurationService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
) {
|
) {
|
||||||
|
this.defaultFilterCount = this.appConfig.search.filterPlaceholdersCount ?? 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -125,4 +133,10 @@ export class SearchFiltersComponent implements OnInit {
|
|||||||
this.searchFilterService.minimizeAll();
|
this.searchFilterService.minimizeAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countFiltersWithComputedVisibility(computed: boolean) {
|
||||||
|
if (computed) {
|
||||||
|
this.filtersWithComputedVisibility += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
<div class="row flex-nowrap">
|
||||||
|
<div [class.mb-2]="(viewMode$ | async) === ViewMode.ListElement" class="info-skeleton col-12">
|
||||||
|
<ngx-skeleton-loader/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if((viewMode$ | async) === ViewMode.ListElement) {
|
||||||
|
@for (result of loadingResults; track result; let first = $first) {
|
||||||
|
<div [class.my-4]="!first" class="row">
|
||||||
|
@if(showThumbnails) {
|
||||||
|
<div class="col-3 col-md-2">
|
||||||
|
<div class="thumbnail-skeleton position-relative">
|
||||||
|
<ngx-skeleton-loader/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div [class.col-9]="showThumbnails" [class.col-md-10]="showThumbnails" [class.col-md-12]="!showThumbnails">
|
||||||
|
<div class="badge-skeleton">
|
||||||
|
<ngx-skeleton-loader/>
|
||||||
|
</div>
|
||||||
|
<div class="text-skeleton">
|
||||||
|
<ngx-skeleton-loader [count]="textLineCount"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} @else if ((viewMode$ | async) === ViewMode.GridElement) {
|
||||||
|
<div class="card-columns row">
|
||||||
|
@for (result of loadingResults; track result) {
|
||||||
|
<div class="card-column col col-sm-6 col-lg-4">
|
||||||
|
<div class="card-skeleton">
|
||||||
|
<ngx-skeleton-loader/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@@ -0,0 +1,32 @@
|
|||||||
|
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<SearchResultsSkeletonComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SearchResultsSkeletonComponent, NgxSkeletonLoaderModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: SearchService, useValue: new SearchServiceStub() },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SearchResultsSkeletonComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
AsyncPipe,
|
||||||
|
NgForOf,
|
||||||
|
} from '@angular/common';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
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',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgxSkeletonLoaderModule,
|
||||||
|
AsyncPipe,
|
||||||
|
NgForOf,
|
||||||
|
],
|
||||||
|
templateUrl: './search-results-skeleton.component.html',
|
||||||
|
styleUrl: './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<ViewMode>;
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,15 @@
|
|||||||
|
@if ((activeFilters$ | async).length > 0 && (appliedFilters$ | async).length === 0) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="filters-badge-skeleton-container">
|
||||||
|
<div class="filter-badge-skeleton">
|
||||||
|
<ngx-skeleton-loader [count]="(activeFilters$ | async).length" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<h1 *ngIf="!disableHeader">{{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}</h1>
|
<h1 *ngIf="!disableHeader">{{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}</h1>
|
||||||
<ds-search-export-csv *ngIf="showCsvExport" [searchConfig]="searchConfig"></ds-search-export-csv>
|
<ds-search-export-csv *ngIf="showCsvExport" [searchConfig]="searchConfig"></ds-search-export-csv>
|
||||||
@@ -19,7 +31,13 @@
|
|||||||
(selectObject)="selectObject.emit($event)">
|
(selectObject)="selectObject.emit($event)">
|
||||||
</ds-viewable-collection>
|
</ds-viewable-collection>
|
||||||
</div>
|
</div>
|
||||||
<ds-loading *ngIf="isLoading()" message="{{'loading.search-results' | translate}}"></ds-loading>
|
|
||||||
|
<ds-search-results-skeleton
|
||||||
|
*ngIf="isLoading()"
|
||||||
|
[showThumbnails]="showThumbnails"
|
||||||
|
[numberOfResults]="searchConfig.pagination.pageSize"
|
||||||
|
></ds-search-results-skeleton>
|
||||||
|
|
||||||
<ds-error *ngIf="showError()"
|
<ds-error *ngIf="showError()"
|
||||||
message="{{errorMessageLabel() | translate}}"></ds-error>
|
message="{{errorMessageLabel() | translate}}"></ds-error>
|
||||||
<div *ngIf="searchResults?.payload?.page.length === 0 || searchResults?.statusCode === 400">
|
<div *ngIf="searchResults?.payload?.page.length === 0 || searchResults?.statusCode === 400">
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -13,16 +13,20 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { Community } from '../../../core/shared/community.model';
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
|
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||||
import { ErrorComponent } from '../../error/error.component';
|
import { ErrorComponent } from '../../error/error.component';
|
||||||
import { ThemedLoadingComponent } from '../../loading/themed-loading.component';
|
|
||||||
import { getMockThemeService } from '../../mocks/theme-service.mock';
|
import { getMockThemeService } from '../../mocks/theme-service.mock';
|
||||||
import { ObjectCollectionComponent } from '../../object-collection/object-collection.component';
|
import { ObjectCollectionComponent } from '../../object-collection/object-collection.component';
|
||||||
import { createFailedRemoteDataObject } from '../../remote-data.utils';
|
import { createFailedRemoteDataObject } from '../../remote-data.utils';
|
||||||
import { ActivatedRouteStub } from '../../testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../testing/active-router.stub';
|
||||||
import { QueryParamsDirectiveStub } from '../../testing/query-params-directive.stub';
|
import { QueryParamsDirectiveStub } from '../../testing/query-params-directive.stub';
|
||||||
|
import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub';
|
||||||
|
import { SearchServiceStub } from '../../testing/search-service.stub';
|
||||||
import { ThemeService } from '../../theme-support/theme.service';
|
import { ThemeService } from '../../theme-support/theme.service';
|
||||||
import { SearchExportCsvComponent } from '../search-export-csv/search-export-csv.component';
|
import { SearchExportCsvComponent } from '../search-export-csv/search-export-csv.component';
|
||||||
import { SearchResultsComponent } from './search-results.component';
|
import { SearchResultsComponent } from './search-results.component';
|
||||||
|
import { SearchResultsSkeletonComponent } from './search-results-skeleton/search-results-skeleton.component';
|
||||||
|
|
||||||
describe('SearchResultsComponent', () => {
|
describe('SearchResultsComponent', () => {
|
||||||
let comp: SearchResultsComponent;
|
let comp: SearchResultsComponent;
|
||||||
@@ -35,6 +39,11 @@ describe('SearchResultsComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
|
{ provide: SearchService, useValue: new SearchServiceStub() },
|
||||||
|
{
|
||||||
|
provide: SearchConfigurationService,
|
||||||
|
useValue: new SearchConfigurationServiceStub(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
@@ -48,8 +57,8 @@ describe('SearchResultsComponent', () => {
|
|||||||
imports: [
|
imports: [
|
||||||
SearchExportCsvComponent,
|
SearchExportCsvComponent,
|
||||||
ObjectCollectionComponent,
|
ObjectCollectionComponent,
|
||||||
ThemedLoadingComponent,
|
|
||||||
ErrorComponent,
|
ErrorComponent,
|
||||||
|
SearchResultsSkeletonComponent,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
add: { imports: [QueryParamsDirectiveStub] },
|
add: { imports: [QueryParamsDirectiveStub] },
|
||||||
@@ -96,7 +105,7 @@ describe('SearchResultsComponent', () => {
|
|||||||
|
|
||||||
it('should display link with new search where query is quoted if search return a error 400', () => {
|
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).searchResults = createFailedRemoteDataObject('Error', 400);
|
||||||
(comp as any).searchConfig = { query: 'foobar' };
|
(comp as any).searchConfig = { query: 'foobar', pagination: { pageSize: 10 } };
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const linkDes = fixture.debugElement.queryAll(By.directive(QueryParamsDirectiveStub));
|
const linkDes = fixture.debugElement.queryAll(By.directive(QueryParamsDirectiveStub));
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { NgIf } from '@angular/common';
|
import {
|
||||||
|
AsyncPipe,
|
||||||
|
NgIf,
|
||||||
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@@ -7,12 +10,19 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
Observable,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { Context } from '../../../core/shared/context.model';
|
import { Context } from '../../../core/shared/context.model';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
|
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||||
import {
|
import {
|
||||||
fadeIn,
|
fadeIn,
|
||||||
@@ -23,13 +33,15 @@ import {
|
|||||||
isNotEmpty,
|
isNotEmpty,
|
||||||
} from '../../empty.util';
|
} from '../../empty.util';
|
||||||
import { ErrorComponent } from '../../error/error.component';
|
import { ErrorComponent } from '../../error/error.component';
|
||||||
import { ThemedLoadingComponent } from '../../loading/themed-loading.component';
|
|
||||||
import { CollectionElementLinkType } from '../../object-collection/collection-element-link.type';
|
import { CollectionElementLinkType } from '../../object-collection/collection-element-link.type';
|
||||||
import { ObjectCollectionComponent } from '../../object-collection/object-collection.component';
|
import { ObjectCollectionComponent } from '../../object-collection/object-collection.component';
|
||||||
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||||
|
import { AppliedFilter } from '../models/applied-filter.model';
|
||||||
import { PaginatedSearchOptions } from '../models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../models/paginated-search-options.model';
|
||||||
|
import { SearchFilter } from '../models/search-filter.model';
|
||||||
import { SearchResult } from '../models/search-result.model';
|
import { SearchResult } from '../models/search-result.model';
|
||||||
import { SearchExportCsvComponent } from '../search-export-csv/search-export-csv.component';
|
import { SearchExportCsvComponent } from '../search-export-csv/search-export-csv.component';
|
||||||
|
import { SearchResultsSkeletonComponent } from './search-results-skeleton/search-results-skeleton.component';
|
||||||
|
|
||||||
export interface SelectionConfig {
|
export interface SelectionConfig {
|
||||||
repeatable: boolean;
|
repeatable: boolean;
|
||||||
@@ -39,12 +51,13 @@ export interface SelectionConfig {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-base-search-results',
|
selector: 'ds-base-search-results',
|
||||||
templateUrl: './search-results.component.html',
|
templateUrl: './search-results.component.html',
|
||||||
|
styleUrls: ['./search-results.component.scss'],
|
||||||
animations: [
|
animations: [
|
||||||
fadeIn,
|
fadeIn,
|
||||||
fadeInOut,
|
fadeInOut,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, SearchExportCsvComponent, ObjectCollectionComponent, ThemedLoadingComponent, ErrorComponent, RouterLink, TranslateModule],
|
imports: [NgIf, SearchExportCsvComponent, ObjectCollectionComponent, ErrorComponent, RouterLink, TranslateModule, SearchResultsSkeletonComponent, AsyncPipe, NgxSkeletonLoaderModule],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +65,15 @@ export interface SelectionConfig {
|
|||||||
*/
|
*/
|
||||||
export class SearchResultsComponent {
|
export class SearchResultsComponent {
|
||||||
hasNoValue = hasNoValue;
|
hasNoValue = hasNoValue;
|
||||||
|
/**
|
||||||
|
* Currently active filters in url
|
||||||
|
*/
|
||||||
|
activeFilters$: Observable<SearchFilter[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter applied to show labels, once populated the activeFilters$ will be loaded
|
||||||
|
*/
|
||||||
|
appliedFilters$: BehaviorSubject<AppliedFilter[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The link type of the listed search results
|
* The link type of the listed search results
|
||||||
@@ -125,10 +147,18 @@ export class SearchResultsComponent {
|
|||||||
|
|
||||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected searchConfigService: SearchConfigurationService,
|
||||||
|
protected searchService: SearchService,
|
||||||
|
) {
|
||||||
|
this.activeFilters$ = this.searchConfigService.getCurrentFilters();
|
||||||
|
this.appliedFilters$ = this.searchService.appliedFilters$;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if search results are loading
|
* Check if search results are loading
|
||||||
*/
|
*/
|
||||||
isLoading() {
|
isLoading(): boolean {
|
||||||
return !this.showError() && (hasNoValue(this.searchResults) || hasNoValue(this.searchResults.payload) || this.searchResults.isLoading);
|
return !this.showError() && (hasNoValue(this.searchResults) || hasNoValue(this.searchResults.payload) || this.searchResults.isLoading);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
NO_ERRORS_SCHEMA,
|
NO_ERRORS_SCHEMA,
|
||||||
|
PLATFORM_ID,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
@@ -246,6 +247,7 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar
|
|||||||
},
|
},
|
||||||
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||||
{ provide: APP_CONFIG, useValue: environment },
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
}).overrideComponent(compType, {
|
}).overrideComponent(compType, {
|
||||||
@@ -415,5 +417,34 @@ describe('SearchComponent', () => {
|
|||||||
expect(result).toBeNull();
|
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();
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
isPlatformServer,
|
||||||
NgIf,
|
NgIf,
|
||||||
NgTemplateOutlet,
|
NgTemplateOutlet,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
|
PLATFORM_ID,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
NavigationStart,
|
NavigationStart,
|
||||||
@@ -37,6 +39,7 @@ import {
|
|||||||
APP_CONFIG,
|
APP_CONFIG,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
} from '../../../config/app-config.interface';
|
} from '../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-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 { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths';
|
||||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
@@ -236,6 +239,11 @@ export class SearchComponent implements OnDestroy, OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() hideScopeInUrl: boolean;
|
@Input() hideScopeInUrl: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines whether to fetch search results during SSR execution
|
||||||
|
*/
|
||||||
|
@Input() renderOnServerSide: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current configuration used during the search
|
* The current configuration used during the search
|
||||||
*/
|
*/
|
||||||
@@ -251,6 +259,7 @@ export class SearchComponent implements OnDestroy, OnInit {
|
|||||||
*/
|
*/
|
||||||
currentScope$: Observable<string>;
|
currentScope$: Observable<string>;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current sort options used
|
* The current sort options used
|
||||||
*/
|
*/
|
||||||
@@ -345,6 +354,7 @@ export class SearchComponent implements OnDestroy, OnInit {
|
|||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
@Inject(PLATFORM_ID) public platformId: any,
|
||||||
) {
|
) {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
}
|
}
|
||||||
@@ -357,6 +367,14 @@ export class SearchComponent implements OnDestroy, OnInit {
|
|||||||
* If something changes, update the list of scopes for the dropdown
|
* If something changes, update the list of scopes for the dropdown
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
if (!this.renderOnServerSide && !environment.ssr.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) {
|
if (this.useUniquePageId) {
|
||||||
// Create an unique pagination id related to the instance of the SearchComponent
|
// Create an unique pagination id related to the instance of the SearchComponent
|
||||||
this.paginationId = uniqueId(this.paginationId);
|
this.paginationId = uniqueId(this.paginationId);
|
||||||
|
@@ -26,6 +26,10 @@ export class SearchConfigurationServiceStub {
|
|||||||
return observableOf([]);
|
return observableOf([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCurrentFilters() {
|
||||||
|
return observableOf([]);
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentScope(a) {
|
getCurrentScope(a) {
|
||||||
return observableOf('test-id');
|
return observableOf('test-id');
|
||||||
}
|
}
|
||||||
|
@@ -516,6 +516,7 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
filter: ['title', 'author', 'subject', 'entityType'],
|
filter: ['title', 'author', 'subject', 'entityType'],
|
||||||
},
|
},
|
||||||
|
filterPlaceholdersCount: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
notifyMetrics: AdminNotifyMetricsRow[] = [
|
notifyMetrics: AdminNotifyMetricsRow[] = [
|
||||||
|
@@ -8,5 +8,11 @@ export interface SearchConfig extends Config {
|
|||||||
* Used by {@link UploadBitstreamComponent}.
|
* Used by {@link UploadBitstreamComponent}.
|
||||||
*/
|
*/
|
||||||
advancedFilters: AdvancedSearchConfig;
|
advancedFilters: AdvancedSearchConfig;
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
|
@@ -25,4 +25,14 @@ export interface SSRConfig extends Config {
|
|||||||
* Paths to enable SSR for. Defaults to the home page and paths in the sitemap.
|
* Paths to enable SSR for. Defaults to the home page and paths in the sitemap.
|
||||||
*/
|
*/
|
||||||
paths: Array<string>;
|
paths: Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enable rendering of search component on SSR
|
||||||
|
*/
|
||||||
|
enableSearchComponent: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enable rendering of browse component on SSR
|
||||||
|
*/
|
||||||
|
enableBrowseComponent: boolean;
|
||||||
}
|
}
|
||||||
|
@@ -9,5 +9,7 @@ export const environment: Partial<BuildConfig> = {
|
|||||||
enablePerformanceProfiler: false,
|
enablePerformanceProfiler: false,
|
||||||
inlineCriticalCss: false,
|
inlineCriticalCss: false,
|
||||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
|
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
|
||||||
|
enableSearchComponent: false,
|
||||||
|
enableBrowseComponent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -13,6 +13,8 @@ export const environment: BuildConfig = {
|
|||||||
enablePerformanceProfiler: false,
|
enablePerformanceProfiler: false,
|
||||||
inlineCriticalCss: false,
|
inlineCriticalCss: false,
|
||||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
|
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
|
||||||
|
enableSearchComponent: false,
|
||||||
|
enableBrowseComponent: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Angular express server settings.
|
// Angular express server settings.
|
||||||
|
@@ -14,6 +14,8 @@ export const environment: Partial<BuildConfig> = {
|
|||||||
enablePerformanceProfiler: false,
|
enablePerformanceProfiler: false,
|
||||||
inlineCriticalCss: false,
|
inlineCriticalCss: false,
|
||||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
|
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
|
||||||
|
enableSearchComponent: false,
|
||||||
|
enableBrowseComponent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -150,4 +150,16 @@
|
|||||||
|
|
||||||
--green1: #1c710a; // This variable represents the success color for the Orejime cookie banner
|
--green1: #1c710a; // This variable represents the success color for the Orejime cookie banner
|
||||||
--button-text-color-cookie: #fff; // This variable represents the text color for buttons in the Orejime cookie banner
|
--button-text-color-cookie: #fff; // This variable represents the text color for buttons in the Orejime cookie banner
|
||||||
|
|
||||||
|
--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;
|
||||||
}
|
}
|
||||||
|
@@ -5,18 +5,14 @@
|
|||||||
*
|
*
|
||||||
* https://www.atmire.com/software-license/
|
* https://www.atmire.com/software-license/
|
||||||
*/
|
*/
|
||||||
import {
|
import { AsyncPipe } from '@angular/common';
|
||||||
AsyncPipe,
|
|
||||||
NgFor,
|
|
||||||
NgIf,
|
|
||||||
} from '@angular/common';
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { SearchConfigurationService } from '../../../../../../app/core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../../../app/core/shared/search/search-configuration.service';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../../app/my-dspace-page/my-dspace-configuration.service';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../../../app/my-dspace-page/my-dspace-configuration.service';
|
||||||
import { AdvancedSearchComponent } from '../../../../../../app/shared/search/advanced-search/advanced-search.component';
|
|
||||||
import { SearchFilterComponent } from '../../../../../../app/shared/search/search-filters/search-filter/search-filter.component';
|
import { SearchFilterComponent } from '../../../../../../app/shared/search/search-filters/search-filter/search-filter.component';
|
||||||
import { SearchFiltersComponent as BaseComponent } from '../../../../../../app/shared/search/search-filters/search-filters.component';
|
import { SearchFiltersComponent as BaseComponent } from '../../../../../../app/shared/search/search-filters/search-filters.component';
|
||||||
|
|
||||||
@@ -34,7 +30,7 @@ import { SearchFiltersComponent as BaseComponent } from '../../../../../../app/s
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, NgFor, SearchFilterComponent, RouterLink, AsyncPipe, TranslateModule, AdvancedSearchComponent],
|
imports: [SearchFilterComponent, RouterLink, AsyncPipe, TranslateModule, NgxSkeletonLoaderModule],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class SearchFiltersComponent extends BaseComponent {
|
export class SearchFiltersComponent extends BaseComponent {
|
||||||
|
@@ -1,29 +1,33 @@
|
|||||||
import { NgIf } from '@angular/common';
|
import {
|
||||||
|
AsyncPipe,
|
||||||
|
NgIf,
|
||||||
|
} from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fadeIn,
|
fadeIn,
|
||||||
fadeInOut,
|
fadeInOut,
|
||||||
} from '../../../../../../app/shared/animations/fade';
|
} from '../../../../../../app/shared/animations/fade';
|
||||||
import { ErrorComponent } from '../../../../../../app/shared/error/error.component';
|
import { ErrorComponent } from '../../../../../../app/shared/error/error.component';
|
||||||
import { ThemedLoadingComponent } from '../../../../../../app/shared/loading/themed-loading.component';
|
|
||||||
import { ObjectCollectionComponent } from '../../../../../../app/shared/object-collection/object-collection.component';
|
import { ObjectCollectionComponent } from '../../../../../../app/shared/object-collection/object-collection.component';
|
||||||
import { SearchExportCsvComponent } from '../../../../../../app/shared/search/search-export-csv/search-export-csv.component';
|
import { SearchExportCsvComponent } from '../../../../../../app/shared/search/search-export-csv/search-export-csv.component';
|
||||||
import { SearchResultsComponent as BaseComponent } from '../../../../../../app/shared/search/search-results/search-results.component';
|
import { SearchResultsComponent as BaseComponent } from '../../../../../../app/shared/search/search-results/search-results.component';
|
||||||
|
import { SearchResultsSkeletonComponent } from '../../../../../../app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-themed-search-results',
|
selector: 'ds-themed-search-results',
|
||||||
// templateUrl: './search-results.component.html',
|
// templateUrl: './search-results.component.html',
|
||||||
templateUrl: '../../../../../../app/shared/search/search-results/search-results.component.html',
|
templateUrl: '../../../../../../app/shared/search/search-results/search-results.component.html',
|
||||||
// styleUrls: ['./search-results.component.scss'],
|
styleUrls: ['../../../../../../app/shared/search/search-results/search-results.component.scss'],
|
||||||
animations: [
|
animations: [
|
||||||
fadeIn,
|
fadeIn,
|
||||||
fadeInOut,
|
fadeInOut,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, SearchExportCsvComponent, ObjectCollectionComponent, ThemedLoadingComponent, ErrorComponent, RouterLink, TranslateModule],
|
imports: [NgIf, SearchExportCsvComponent, ObjectCollectionComponent, ErrorComponent, RouterLink, TranslateModule, SearchResultsSkeletonComponent, SearchResultsSkeletonComponent, AsyncPipe, NgxSkeletonLoaderModule],
|
||||||
})
|
})
|
||||||
export class SearchResultsComponent extends BaseComponent {
|
export class SearchResultsComponent extends BaseComponent {
|
||||||
|
|
||||||
|
@@ -12,6 +12,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
|
||||||
import { RootModule } from '../../app/root.module';
|
import { RootModule } from '../../app/root.module';
|
||||||
|
import { SearchResultsSkeletonComponent } from '../../app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component';
|
||||||
import { MetadataImportPageComponent } from './app/admin/admin-import-metadata-page/metadata-import-page.component';
|
import { MetadataImportPageComponent } from './app/admin/admin-import-metadata-page/metadata-import-page.component';
|
||||||
import { AdminSearchPageComponent } from './app/admin/admin-search-page/admin-search-page.component';
|
import { AdminSearchPageComponent } from './app/admin/admin-search-page/admin-search-page.component';
|
||||||
import { AdminSidebarComponent } from './app/admin/admin-sidebar/admin-sidebar.component';
|
import { AdminSidebarComponent } from './app/admin/admin-sidebar/admin-sidebar.component';
|
||||||
@@ -105,6 +106,7 @@ import { WorkflowItemDeleteComponent } from './app/workflowitems-edit-page/workf
|
|||||||
import { WorkflowItemSendBackComponent } from './app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component';
|
import { WorkflowItemSendBackComponent } from './app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component';
|
||||||
import { WorkspaceItemsDeletePageComponent } from './app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component';
|
import { WorkspaceItemsDeletePageComponent } from './app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component';
|
||||||
|
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
FileSectionComponent,
|
FileSectionComponent,
|
||||||
HomePageComponent,
|
HomePageComponent,
|
||||||
@@ -198,6 +200,7 @@ const DECLARATIONS = [
|
|||||||
ComcolPageContentComponent,
|
ComcolPageContentComponent,
|
||||||
AdminSearchPageComponent,
|
AdminSearchPageComponent,
|
||||||
AdminWorkflowPageComponent,
|
AdminWorkflowPageComponent,
|
||||||
|
SearchResultsSkeletonComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
/* set the next two properties as `--ds-header-navbar-border-bottom-*`
|
/* 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 */
|
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-color: #{$white};
|
||||||
--ds-expandable-navbar-border-top-height: 0;
|
--ds-expandable-navbar-border-top-height: 0;
|
||||||
--ds-expandable-navbar-padding-top: 0;
|
--ds-expandable-navbar-padding-top: 0;
|
||||||
|
Reference in New Issue
Block a user