mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #3886 from tdonohue/port_3709_to_main
[Port main] Exclude search and browse from Angular SSR (#3709)
This commit is contained in:
@@ -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
|
||||
|
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@@ -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);
|
||||
|
@@ -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">
|
||||
<ds-browse-by *ngIf="(loading$ | async) !== true" class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:{
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
NO_ERRORS_SCHEMA,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
fakeAsync,
|
||||
TestBed,
|
||||
tick,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
@@ -147,6 +152,7 @@ describe('BrowseByMetadataComponent', () => {
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
{ provide: SelectableListService, useValue: {} },
|
||||
{ provide: HostWindowService, useValue: {} },
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
})
|
||||
@@ -259,6 +265,35 @@ describe('BrowseByMetadataComponent', () => {
|
||||
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>>> {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
isPlatformServer,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
@@ -33,6 +35,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 { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||
@@ -114,6 +117,11 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||
*/
|
||||
@Input() displayTitle = true;
|
||||
|
||||
/**
|
||||
* Defines whether to fetch search results during SSR execution
|
||||
*/
|
||||
@Input() renderOnServerSide: boolean;
|
||||
|
||||
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
|
||||
*/
|
||||
loading$ = observableOf(true);
|
||||
/**
|
||||
* Whether this component should be rendered or not in SSR
|
||||
*/
|
||||
ssrRenderingDisabled = false;
|
||||
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
protected browseService: BrowseService,
|
||||
@@ -202,6 +214,7 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, 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;
|
||||
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||
@@ -209,11 +222,15 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||
currentPage: 1,
|
||||
pageSize: this.appConfig.browseBy.pageSize,
|
||||
});
|
||||
this.ssrRenderingDisabled = !this.renderOnServerSide && !environment.ssr.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);
|
||||
@@ -336,7 +353,6 @@ export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.paginationService.clearPagination(this.paginationConfig.id);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -5,7 +5,9 @@ import {
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
fakeAsync,
|
||||
TestBed,
|
||||
tick,
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import {
|
||||
@@ -23,6 +25,7 @@ import { BrowseService } from '../../core/browse/browse.service';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { ItemDataService } from '../../core/data/item-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';
|
||||
@@ -81,6 +84,7 @@ describe('BrowseByTitleComponent', () => {
|
||||
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({}),
|
||||
queryParams: observableOf({}),
|
||||
data: observableOf({ metadata: 'title' }),
|
||||
});
|
||||
|
||||
@@ -127,4 +131,35 @@ describe('BrowseByTitleComponent', () => {
|
||||
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 {
|
||||
AsyncPipe,
|
||||
isPlatformServer,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
@@ -8,12 +9,16 @@ import {
|
||||
} from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
import {
|
||||
SortDirection,
|
||||
SortOptions,
|
||||
@@ -59,6 +64,10 @@ import {
|
||||
export class BrowseByTitleComponent extends BrowseByMetadataComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.renderOnServerSide && !environment.ssr.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);
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@@ -57,7 +58,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);
|
||||
}
|
||||
}
|
||||
|
@@ -6,11 +6,13 @@ import {
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
@@ -78,6 +80,8 @@ export class SearchFilterComponent implements OnInit, OnChanges, OnDestroy {
|
||||
*/
|
||||
@Input() scope: string;
|
||||
|
||||
@Output() isVisibilityComputed = new EventEmitter<boolean>();
|
||||
|
||||
/**
|
||||
* 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.collapsed$ = this.isCollapsed();
|
||||
this.initializeFilter();
|
||||
this.subs.push(this.appliedFilters$.pipe(take(1)).subscribe((selectedValues: AppliedFilter[]) => {
|
||||
if (isNotEmpty(selectedValues)) {
|
||||
this.filterService.expand(this.filter.name);
|
||||
}
|
||||
}));
|
||||
this.subs.push(
|
||||
this.appliedFilters$.subscribe((selectedValues: AppliedFilter[]) => {
|
||||
if (isNotEmpty(selectedValues)) {
|
||||
this.filterService.expand(this.filter.name);
|
||||
}
|
||||
}),
|
||||
this.getIsActive().pipe(take(1)).subscribe(() => {
|
||||
this.isVisibilityComputed.emit(true);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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([
|
||||
this.appliedFilters$,
|
||||
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>
|
||||
<h2 *ngIf="!inPlaceSearch">{{filterLabel+'.filters.head' | translate}}</h2>
|
||||
<div *ngIf="(filters | async)?.hasSucceeded">
|
||||
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
|
||||
<ds-search-filter [scope]="currentScope" [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter>
|
||||
</div>
|
||||
</div>
|
||||
<button *ngIf="inPlaceSearch" 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>
|
||||
@if (inPlaceSearch) {
|
||||
<h3>{{filterLabel+'.filters.head' | translate}}</h3>
|
||||
} @else {
|
||||
<h2>{{filterLabel+'.filters.head' | translate}}</h2>
|
||||
}
|
||||
|
||||
@if ((filters | async)?.hasSucceeded) {
|
||||
<div [class.visually-hidden]="filtersWithComputedVisibility !== (filters | async)?.payload?.length">
|
||||
@for (filter of (filters | async)?.payload; track filter.name) {
|
||||
<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/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 { 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 { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service';
|
||||
@@ -41,6 +43,7 @@ describe('SearchFiltersComponent', () => {
|
||||
{ provide: SearchService, useValue: searchService },
|
||||
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
|
||||
{ provide: SearchFilterService, useValue: searchFilters },
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).overrideComponent(SearchFiltersComponent, {
|
||||
|
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgFor,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Inject,
|
||||
@@ -14,19 +10,23 @@ import {
|
||||
RouterLink,
|
||||
} from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../../config/app-config.interface';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { SearchService } from '../../../core/shared/search/search.service';
|
||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service';
|
||||
import { currentPath } from '../../utils/route.utils';
|
||||
import { AdvancedSearchComponent } from '../advanced-search/advanced-search.component';
|
||||
import { AppliedFilter } from '../models/applied-filter.model';
|
||||
import { SearchFilterConfig } from '../models/search-filter-config.model';
|
||||
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'],
|
||||
templateUrl: './search-filters.component.html',
|
||||
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;
|
||||
|
||||
/**
|
||||
* Filters for which visibility has been computed
|
||||
*/
|
||||
filtersWithComputedVisibility = 0;
|
||||
|
||||
subs = [];
|
||||
filterLabel = 'search';
|
||||
defaultFilterCount: number;
|
||||
|
||||
constructor(
|
||||
protected searchService: SearchService,
|
||||
protected searchFilterService: SearchFilterService,
|
||||
protected router: Router,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) protected searchConfigService: SearchConfigurationService,
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
) {
|
||||
this.defaultFilterCount = this.appConfig.search.filterPlaceholdersCount ?? 5;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -125,4 +133,10 @@ export class SearchFiltersComponent implements OnInit {
|
||||
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">
|
||||
<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>
|
||||
@@ -19,7 +31,13 @@
|
||||
(selectObject)="selectObject.emit($event)">
|
||||
</ds-viewable-collection>
|
||||
</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()"
|
||||
message="{{errorMessageLabel() | translate}}"></ds-error>
|
||||
<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 { 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 { ThemedLoadingComponent } from '../../loading/themed-loading.component';
|
||||
import { getMockThemeService } from '../../mocks/theme-service.mock';
|
||||
import { ObjectCollectionComponent } from '../../object-collection/object-collection.component';
|
||||
import { createFailedRemoteDataObject } from '../../remote-data.utils';
|
||||
import { ActivatedRouteStub } from '../../testing/active-router.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 { SearchExportCsvComponent } from '../search-export-csv/search-export-csv.component';
|
||||
import { SearchResultsComponent } from './search-results.component';
|
||||
import { SearchResultsSkeletonComponent } from './search-results-skeleton/search-results-skeleton.component';
|
||||
|
||||
describe('SearchResultsComponent', () => {
|
||||
let comp: SearchResultsComponent;
|
||||
@@ -35,6 +39,11 @@ describe('SearchResultsComponent', () => {
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
{ provide: SearchService, useValue: new SearchServiceStub() },
|
||||
{
|
||||
provide: SearchConfigurationService,
|
||||
useValue: new SearchConfigurationServiceStub(),
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
@@ -48,8 +57,8 @@ describe('SearchResultsComponent', () => {
|
||||
imports: [
|
||||
SearchExportCsvComponent,
|
||||
ObjectCollectionComponent,
|
||||
ThemedLoadingComponent,
|
||||
ErrorComponent,
|
||||
SearchResultsSkeletonComponent,
|
||||
],
|
||||
},
|
||||
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', () => {
|
||||
(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));
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -7,12 +10,19 @@ import {
|
||||
} from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
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 { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Context } from '../../../core/shared/context.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 {
|
||||
fadeIn,
|
||||
@@ -23,13 +33,15 @@ import {
|
||||
isNotEmpty,
|
||||
} from '../../empty.util';
|
||||
import { ErrorComponent } from '../../error/error.component';
|
||||
import { ThemedLoadingComponent } from '../../loading/themed-loading.component';
|
||||
import { CollectionElementLinkType } from '../../object-collection/collection-element-link.type';
|
||||
import { ObjectCollectionComponent } from '../../object-collection/object-collection.component';
|
||||
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 { SearchFilter } from '../models/search-filter.model';
|
||||
import { SearchResult } from '../models/search-result.model';
|
||||
import { SearchExportCsvComponent } from '../search-export-csv/search-export-csv.component';
|
||||
import { SearchResultsSkeletonComponent } from './search-results-skeleton/search-results-skeleton.component';
|
||||
|
||||
export interface SelectionConfig {
|
||||
repeatable: boolean;
|
||||
@@ -39,12 +51,13 @@ export interface SelectionConfig {
|
||||
@Component({
|
||||
selector: 'ds-base-search-results',
|
||||
templateUrl: './search-results.component.html',
|
||||
styleUrls: ['./search-results.component.scss'],
|
||||
animations: [
|
||||
fadeIn,
|
||||
fadeInOut,
|
||||
],
|
||||
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 {
|
||||
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
|
||||
@@ -125,10 +147,18 @@ export class SearchResultsComponent {
|
||||
|
||||
@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
|
||||
*/
|
||||
isLoading() {
|
||||
isLoading(): boolean {
|
||||
return !this.showError() && (hasNoValue(this.searchResults) || hasNoValue(this.searchResults.payload) || this.searchResults.isLoading);
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
NO_ERRORS_SCHEMA,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
@@ -246,6 +247,7 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar
|
||||
},
|
||||
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).overrideComponent(compType, {
|
||||
@@ -415,5 +417,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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
isPlatformServer,
|
||||
NgIf,
|
||||
NgTemplateOutlet,
|
||||
} from '@angular/common';
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
NavigationStart,
|
||||
@@ -37,6 +39,7 @@ import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../config/app-config.interface';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths';
|
||||
import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths';
|
||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
@@ -236,6 +239,11 @@ export class SearchComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
@Input() hideScopeInUrl: boolean;
|
||||
|
||||
/**
|
||||
* Defines whether to fetch search results during SSR execution
|
||||
*/
|
||||
@Input() renderOnServerSide: boolean;
|
||||
|
||||
/**
|
||||
* The current configuration used during the search
|
||||
*/
|
||||
@@ -251,6 +259,7 @@ export class SearchComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
currentScope$: Observable<string>;
|
||||
|
||||
|
||||
/**
|
||||
* The current sort options used
|
||||
*/
|
||||
@@ -345,6 +354,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();
|
||||
}
|
||||
@@ -357,6 +367,14 @@ export class SearchComponent implements OnDestroy, OnInit {
|
||||
* If something changes, update the list of scopes for the dropdown
|
||||
*/
|
||||
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) {
|
||||
// Create an unique pagination id related to the instance of the SearchComponent
|
||||
this.paginationId = uniqueId(this.paginationId);
|
||||
|
@@ -26,6 +26,10 @@ export class SearchConfigurationServiceStub {
|
||||
return observableOf([]);
|
||||
}
|
||||
|
||||
getCurrentFilters() {
|
||||
return observableOf([]);
|
||||
}
|
||||
|
||||
getCurrentScope(a) {
|
||||
return observableOf('test-id');
|
||||
}
|
||||
|
@@ -516,6 +516,7 @@ export class DefaultAppConfig implements AppConfig {
|
||||
enabled: false,
|
||||
filter: ['title', 'author', 'subject', 'entityType'],
|
||||
},
|
||||
filterPlaceholdersCount: 5,
|
||||
};
|
||||
|
||||
notifyMetrics: AdminNotifyMetricsRow[] = [
|
||||
|
@@ -8,5 +8,11 @@ export interface SearchConfig extends Config {
|
||||
* Used by {@link UploadBitstreamComponent}.
|
||||
*/
|
||||
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: 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,
|
||||
inlineCriticalCss: false,
|
||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
|
||||
enableSearchComponent: false,
|
||||
enableBrowseComponent: false,
|
||||
},
|
||||
};
|
||||
|
@@ -13,6 +13,8 @@ export const environment: BuildConfig = {
|
||||
enablePerformanceProfiler: false,
|
||||
inlineCriticalCss: false,
|
||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ],
|
||||
enableSearchComponent: false,
|
||||
enableBrowseComponent: false,
|
||||
},
|
||||
|
||||
// Angular express server settings.
|
||||
|
@@ -14,6 +14,8 @@ export const environment: Partial<BuildConfig> = {
|
||||
enablePerformanceProfiler: false,
|
||||
inlineCriticalCss: false,
|
||||
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
|
||||
--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/
|
||||
*/
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgFor,
|
||||
NgIf,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { SearchConfigurationService } from '../../../../../../app/core/shared/search/search-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 { 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,
|
||||
imports: [NgIf, NgFor, SearchFilterComponent, RouterLink, AsyncPipe, TranslateModule, AdvancedSearchComponent],
|
||||
imports: [SearchFilterComponent, RouterLink, AsyncPipe, TranslateModule, NgxSkeletonLoaderModule],
|
||||
})
|
||||
|
||||
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 { RouterLink } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import {
|
||||
fadeIn,
|
||||
fadeInOut,
|
||||
} from '../../../../../../app/shared/animations/fade';
|
||||
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 { 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 { SearchResultsSkeletonComponent } from '../../../../../../app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-search-results',
|
||||
// templateUrl: './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: [
|
||||
fadeIn,
|
||||
fadeInOut,
|
||||
],
|
||||
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 {
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||
|
||||
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 { AdminSearchPageComponent } from './app/admin/admin-search-page/admin-search-page.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 { WorkspaceItemsDeletePageComponent } from './app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component';
|
||||
|
||||
|
||||
const DECLARATIONS = [
|
||||
FileSectionComponent,
|
||||
HomePageComponent,
|
||||
@@ -198,6 +200,7 @@ const DECLARATIONS = [
|
||||
ComcolPageContentComponent,
|
||||
AdminSearchPageComponent,
|
||||
AdminWorkflowPageComponent,
|
||||
SearchResultsSkeletonComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user