Skip to content

Commit c10e62f

Browse files
FrancescoMolinaroatarix83
authored andcommitted
Merged in task/dspace-cris-2023_02_x/DSC-2318 (pull request DSpace#3118)
[DSC-2318] configure fallback message on skeleton loader Approved-by: Giuseppe Digilio
2 parents 88a8194 + fc7a9e3 commit c10e62f

6 files changed

Lines changed: 159 additions & 35 deletions

File tree

src/app/shared/loading/loading.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { AlertType } from '../alert/alert-type';
88
import { Router } from '@angular/router';
99
import { isPlatformBrowser, Location } from '@angular/common';
1010

11-
enum MessageType {
11+
export enum MessageType {
1212
LOADING = 'loading',
1313
WARNING = 'warning',
1414
ERROR = 'error'
Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,49 @@
1-
<div class="row flex-nowrap">
2-
<div [class.mb-2]="(viewMode$ | async) === ViewMode.ListElement" class="info-skeleton col-12">
3-
<ngx-skeleton-loader/>
1+
<ng-container *ngIf="warningMessage && loading">
2+
<label aria-live="polite">{{ warningMessage }}</label>
3+
</ng-container>
4+
5+
<ng-container *ngIf="loading; else errorMessageRef">
6+
<div class="row flex-nowrap">
7+
<div [class.mb-2]="(viewMode$ | async) === ViewMode.ListElement" class="info-skeleton col-12">
8+
<ngx-skeleton-loader/>
9+
</div>
410
</div>
5-
</div>
611

7-
<ng-container *ngIf="(viewMode$ | async) === ViewMode.ListElement; else grid">
8-
<ng-container *ngFor="let result of loadingResults; let first = first">
9-
<div [class.my-4]="!first" class="row">
10-
<div *ngIf="showThumbnails" class="col-3 col-md-2">
11-
<div class="thumbnail-skeleton position-relative">
12-
<ngx-skeleton-loader/>
13-
</div>
14-
</div>
15-
<div [class.col-9]="showThumbnails" [class.col-md-10]="showThumbnails" [class.col-md-12]="!showThumbnails">
16-
<div class="badge-skeleton">
17-
<ngx-skeleton-loader/>
12+
<ng-container *ngIf="(viewMode$ | async) === ViewMode.ListElement; else grid">
13+
<ng-container *ngFor="let result of loadingResults; let first = first">
14+
<div [class.my-4]="!first" class="row">
15+
<div *ngIf="showThumbnails" class="col-3 col-md-2">
16+
<div class="thumbnail-skeleton position-relative">
17+
<ngx-skeleton-loader/>
18+
</div>
1819
</div>
19-
<div class="text-skeleton">
20-
<ngx-skeleton-loader [count]="textLineCount"/>
20+
<div [class.col-9]="showThumbnails" [class.col-md-10]="showThumbnails" [class.col-md-12]="!showThumbnails">
21+
<div class="badge-skeleton">
22+
<ngx-skeleton-loader/>
23+
</div>
24+
<div class="text-skeleton">
25+
<ngx-skeleton-loader [count]="textLineCount"/>
26+
</div>
2127
</div>
2228
</div>
23-
</div>
29+
</ng-container>
2430
</ng-container>
25-
</ng-container>
2631

27-
<ng-template #grid>
28-
<div class="card-columns row">
29-
<ng-container *ngFor="let result of loadingResults">
30-
<div class="card-column col col-sm-6 col-lg-4">
31-
<div class="card-skeleton">
32-
<ngx-skeleton-loader/>
32+
<ng-template #grid>
33+
<div class="card-columns row">
34+
<ng-container *ngFor="let result of loadingResults">
35+
<div class="card-column col col-sm-6 col-lg-4">
36+
<div class="card-skeleton">
37+
<ngx-skeleton-loader/>
38+
</div>
3339
</div>
34-
</div>
35-
</ng-container>
36-
</div>
40+
</ng-container>
41+
</div>
42+
</ng-template>
43+
</ng-container>
44+
45+
<ng-template #errorMessageRef>
46+
<ds-alert *ngIf="errorMessage" [type]="AlertTypeEnum.Error" [content]="errorMessage"></ds-alert>
3747
</ng-template>
3848

49+

src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.spec.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,59 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
77
import { SearchService } from '../../../../core/shared/search/search.service';
88
import { SearchServiceStub } from '../../../testing/search-service.stub';
99
import { SearchResultsSkeletonComponent } from './search-results-skeleton.component';
10+
import { By } from '@angular/platform-browser';
11+
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
12+
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
13+
import { APP_CONFIG } from '../../../../../config/app-config.interface';
14+
import { environment } from '../../../../../environments/environment';
15+
import { ChangeDetectorRef } from '@angular/core';
1016

1117
describe('SearchResultsSkeletonComponent', () => {
1218
let component: SearchResultsSkeletonComponent;
1319
let fixture: ComponentFixture<SearchResultsSkeletonComponent>;
1420

1521
beforeEach(async () => {
1622
await TestBed.configureTestingModule({
17-
imports: [NgxSkeletonLoaderModule],
23+
imports: [
24+
NgxSkeletonLoaderModule,
25+
TranslateModule.forRoot({
26+
loader: {
27+
provide: TranslateLoader,
28+
useClass: TranslateLoaderMock,
29+
},
30+
}),
31+
],
1832
declarations: [SearchResultsSkeletonComponent],
1933
providers: [
2034
{ provide: SearchService, useValue: new SearchServiceStub() },
35+
{ provide: APP_CONFIG, useValue: environment },
36+
ChangeDetectorRef,
2137
],
2238
})
2339
.compileComponents();
2440

2541
fixture = TestBed.createComponent(SearchResultsSkeletonComponent);
2642
component = fixture.componentInstance;
43+
component.warningMessage = 'test warning message';
44+
component.errorMessage = 'test error message';
2745
fixture.detectChanges();
2846
});
2947

3048
it('should create', () => {
3149
expect(component).toBeTruthy();
3250
});
51+
52+
it('should display warning message', () => {
53+
component.loading = true;
54+
fixture.detectChanges();
55+
const label = fixture.debugElement.query(By.css('label')).nativeElement;
56+
expect(label.textContent).toContain(component.warningMessage);
57+
});
58+
59+
it('should display error message', () => {
60+
component.loading = false;
61+
fixture.detectChanges();
62+
const alert = fixture.debugElement.query(By.css('ds-alert'));
63+
expect(alert).toBeTruthy();
64+
});
3365
});

src/app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component.ts

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import {
2-
Component,
3-
Input,
2+
ChangeDetectorRef,
3+
Component, Inject,
4+
Input, OnDestroy,
45
OnInit,
56
} from '@angular/core';
6-
import { Observable } from 'rxjs';
7+
import { Observable, Subject, timer } from 'rxjs';
78

89
import { SearchService } from '../../../../core/shared/search/search.service';
910
import { ViewMode } from '../../../../core/shared/view-mode.model';
1011
import { hasValue } from '../../../empty.util';
12+
import { takeUntil } from 'rxjs/operators';
13+
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
14+
import { TranslateService } from '@ngx-translate/core';
15+
import { AlertType } from '../../../alert/alert-type';
1116

1217
@Component({
1318
selector: 'ds-search-results-skeleton',
@@ -17,7 +22,7 @@ import { hasValue } from '../../../empty.util';
1722
/**
1823
* Component to show placeholders for search results while loading, to give a loading feedback to the user without layout shifting.
1924
*/
20-
export class SearchResultsSkeletonComponent implements OnInit {
25+
export class SearchResultsSkeletonComponent implements OnInit, OnDestroy {
2126
/**
2227
* Whether the search result contains thumbnail
2328
*/
@@ -33,6 +38,26 @@ export class SearchResultsSkeletonComponent implements OnInit {
3338
*/
3439
@Input()
3540
textLineCount = 2;
41+
/**
42+
* Whether to show fallback messages after a certain loading time
43+
*/
44+
@Input() showFallbackMessages: boolean;
45+
/**
46+
* The message text for a warning
47+
*/
48+
@Input() warningMessage: string;
49+
/**
50+
* The amount of time to wait for the warning message to be visible
51+
*/
52+
@Input() warningMessageDelay: number;
53+
/**
54+
* The message text for an error
55+
*/
56+
@Input() errorMessage: string;
57+
/**
58+
* The amount of time to wait for the error message to be visible
59+
*/
60+
@Input() errorMessageDelay: number;
3661
/**
3762
* The view mode of the search page
3863
*/
@@ -42,10 +67,32 @@ export class SearchResultsSkeletonComponent implements OnInit {
4267
*/
4368
public loadingResults: number[];
4469

70+
71+
/**
72+
* Whether to show the skeleton loader
73+
*/
74+
public loading = true;
75+
4576
protected readonly ViewMode = ViewMode;
4677

47-
constructor(private searchService: SearchService) {
78+
/**
79+
* Emit on destroy to stop subscriptions
80+
* @private
81+
*/
82+
private destroy$ = new Subject<void>();
83+
84+
readonly AlertTypeEnum = AlertType;
85+
86+
constructor(
87+
private searchService: SearchService,
88+
private translate: TranslateService,
89+
private changeDetectorRef: ChangeDetectorRef,
90+
@Inject(APP_CONFIG) private appConfig: AppConfig,
91+
) {
4892
this.viewMode$ = this.searchService.getViewMode();
93+
this.showFallbackMessages = this.showFallbackMessages ?? this.appConfig.loader.showFallbackMessagesByDefault;
94+
this.warningMessageDelay = this.warningMessageDelay ?? this.appConfig.loader.warningMessageDelay;
95+
this.errorMessageDelay = this.errorMessageDelay ?? this.appConfig.loader.errorMessageDelay;
4996
}
5097

5198
ngOnInit() {
@@ -55,5 +102,35 @@ export class SearchResultsSkeletonComponent implements OnInit {
55102
// this is needed as the default value of show thumbnails is true but set in lower levels of the DOM.
56103
this.showThumbnails = true;
57104
}
105+
106+
this.setTimerForFallbackMessages();
107+
}
108+
109+
ngOnDestroy(): void {
110+
this.destroy$.next();
111+
this.destroy$.complete();
112+
}
113+
114+
setTimerForFallbackMessages(): void {
115+
if (this.showFallbackMessages) {
116+
if (this.warningMessageDelay > 0) {
117+
timer(this.warningMessageDelay)
118+
.pipe(takeUntil(this.destroy$))
119+
.subscribe(() => {
120+
this.warningMessage = this.warningMessage || this.translate.instant('loading.warning');
121+
this.changeDetectorRef.detectChanges();
122+
});
123+
}
124+
125+
if (this.errorMessageDelay > 0) {
126+
timer(this.errorMessageDelay)
127+
.pipe(takeUntil(this.destroy$))
128+
.subscribe(() => {
129+
this.errorMessage = this.errorMessage || this.translate.instant('loading.error');
130+
this.loading = false;
131+
this.changeDetectorRef.detectChanges();
132+
});
133+
}
134+
}
58135
}
59136
}

src/app/shared/search/search-results/search-results.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ <h1 *ngIf="!disableHeader">{{ (configuration ? configuration + '.search.results.
2828

2929
<ds-search-results-skeleton
3030
*ngIf="isLoading()"
31+
[showFallbackMessages]="true"
3132
[showThumbnails]="showThumbnails"
3233
[numberOfResults]="searchConfig.pagination.pageSize"
3334
></ds-search-results-skeleton>

src/app/shared/search/search-results/search-results.component.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { SearchConfigurationService } from '../../../core/shared/search/search-c
1212
import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service.stub';
1313
import { SearchService } from '../../../core/shared/search/search.service';
1414
import { SearchServiceStub } from '../../testing/search-service.stub';
15+
import { APP_CONFIG } from '../../../../config/app-config.interface';
16+
import { environment } from '../../../../environments/environment';
1517

1618
describe('SearchResultsComponent', () => {
1719
let comp: SearchResultsComponent;
@@ -30,6 +32,7 @@ describe('SearchResultsComponent', () => {
3032
providers: [
3133
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
3234
{ provide: SearchService, useValue: new SearchServiceStub() },
35+
{ provide: APP_CONFIG, useValue: environment },
3336
],
3437
schemas: [NO_ERRORS_SCHEMA]
3538
}).compileComponents();

0 commit comments

Comments
 (0)