Merge remote-tracking branch 'origin/main' into DSC-287-Show-an-error-page-if-the-REST-API-is-not-available

# Conflicts:
#	src/app/app-routing.module.ts
This commit is contained in:
Giuseppe Digilio
2021-12-15 15:00:47 +01:00
28 changed files with 100 additions and 39 deletions

View File

@@ -64,7 +64,7 @@ services:
dspacesolr: dspacesolr:
container_name: dspacesolr container_name: dspacesolr
# Uses official Solr image at https://hub.docker.com/_/solr/ # Uses official Solr image at https://hub.docker.com/_/solr/
image: solr:8.8 image: solr:8.11-slim
# Needs main 'dspace' container to start first to guarantee access to solr_configs # Needs main 'dspace' container to start first to guarantee access to solr_configs
depends_on: depends_on:
- dspace - dspace

View File

@@ -62,7 +62,7 @@ services:
dspacesolr: dspacesolr:
container_name: dspacesolr container_name: dspacesolr
# Uses official Solr image at https://hub.docker.com/_/solr/ # Uses official Solr image at https://hub.docker.com/_/solr/
image: solr:8.8 image: solr:8.11-slim
# Needs main 'dspace' container to start first to guarantee access to solr_configs # Needs main 'dspace' container to start first to guarantee access to solr_configs
depends_on: depends_on:
- dspace - dspace

View File

@@ -36,7 +36,7 @@ const ENTRY_COMPONENTS = [
export class AdminSearchModule { export class AdminSearchModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -28,7 +28,7 @@ const ENTRY_COMPONENTS = [
export class AdminWorkflowModuleModule { export class AdminWorkflowModuleModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -34,7 +34,7 @@ const ENTRY_COMPONENTS = [
export class AdminModule { export class AdminModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -218,7 +218,6 @@ import { ServerCheckGuard } from './core/server-check/server-check.guard';
} }
], { ], {
onSameUrlNavigation: 'reload', onSameUrlNavigation: 'reload',
relativeLinkResolution: 'legacy'
}) })
], ],
exports: [RouterModule], exports: [RouterModule],

View File

@@ -31,7 +31,7 @@ const ENTRY_COMPONENTS = [
export class BrowseByModule { export class BrowseByModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -54,7 +54,7 @@ const ENTRY_COMPONENTS = [
export class JournalEntitiesModule { export class JournalEntitiesModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -74,7 +74,7 @@ const COMPONENTS = [
export class ResearchEntitiesModule { export class ResearchEntitiesModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -91,7 +91,7 @@ const DECLARATIONS = [
export class ItemPageModule { export class ItemPageModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -4,7 +4,7 @@
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky" <ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
id="search-sidebar" id="search-sidebar"
[configurationList]="(configurationList$ | async)" [configurationList]="(configurationList$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [resultCount]="(resultsRD$ | async)?.payload?.totalElements"
[viewModeList]="viewModeList" [viewModeList]="viewModeList"
[searchOptions]="(searchOptions$ | async)" [searchOptions]="(searchOptions$ | async)"
[sortOptions]="(sortOptions$ | async)" [sortOptions]="(sortOptions$ | async)"
@@ -27,7 +27,7 @@
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12" <ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
id="search-sidebar-sm" id="search-sidebar-sm"
[configurationList]="(configurationList$ | async)" [configurationList]="(configurationList$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [resultCount]="(resultsRD$ | async)?.payload?.totalElements"
(toggleSidebar)="closeSidebar()" (toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | async)}" [ngClass]="{'active': !(isSidebarCollapsed() | async)}"
[searchOptions]="(searchOptions$ | async)" [searchOptions]="(searchOptions$ | async)"

View File

@@ -19,7 +19,7 @@ import { PaginatedSearchOptions } from '../shared/search/paginated-search-option
import { SearchService } from '../core/shared/search/search.service'; import { SearchService } from '../core/shared/search/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service'; import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model'; import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
import { RoleType } from '../core/roles/role-types'; import { RoleType } from '../core/roles/role-types';
@@ -30,7 +30,7 @@ import { MyDSpaceRequest } from '../core/data/request.models';
import { SearchResult } from '../shared/search/search-result.model'; import { SearchResult } from '../shared/search/search-result.model';
import { Context } from '../core/shared/context.model'; import { Context } from '../core/shared/context.model';
import { SortOptions } from '../core/cache/models/sort-options.model'; import { SortOptions } from '../core/cache/models/sort-options.model';
import { RouteService } from '../core/services/route.service'; import { SearchObjects } from '../shared/search/search-objects.model';
export const MYDSPACE_ROUTE = '/mydspace'; export const MYDSPACE_ROUTE = '/mydspace';
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService'); export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
@@ -111,8 +111,7 @@ export class MyDSpacePageComponent implements OnInit {
constructor(private service: SearchService, constructor(private service: SearchService,
private sidebarService: SidebarService, private sidebarService: SidebarService,
private windowService: HostWindowService, private windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
private routeService: RouteService) {
this.isXsOrSm$ = this.windowService.isXsOrSm(); this.isXsOrSm$ = this.windowService.isXsOrSm();
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest); this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
} }
@@ -134,8 +133,8 @@ export class MyDSpacePageComponent implements OnInit {
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
this.sub = this.searchOptions$.pipe( this.sub = this.searchOptions$.pipe(
tap(() => this.resultsRD$.next(null)), tap(() => this.resultsRD$.next(null)),
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstSucceededRemoteData()))) switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstCompletedRemoteData())))
.subscribe((results) => { .subscribe((results: RemoteData<SearchObjects<DSpaceObject>>) => {
this.resultsRD$.next(results); this.resultsRD$.next(results);
}); });

View File

@@ -10,5 +10,5 @@
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading> <ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>
<ds-error *ngIf="searchResults?.hasFailed && (!searchResults?.errorMessage || searchResults?.statusCode != 400)" message="{{'error.search-results' | translate}}"></ds-error> <ds-error *ngIf="showError()" message="{{errorMessageLabel() | translate}}"></ds-error>
<h3 *ngIf="searchResults?.payload?.page.length == 0" class="text-center text-muted" ><span>{{'mydspace.results.no-results' | translate}}</span></h3> <h3 *ngIf="searchResults?.payload?.page.length == 0" class="text-center text-muted" ><span>{{'mydspace.results.no-results' | translate}}</span></h3>

View File

@@ -40,9 +40,19 @@ describe('MyDSpaceResultsComponent', () => {
expect(fixture.debugElement.query(By.css('a'))).toBeNull(); expect(fixture.debugElement.query(By.css('a'))).toBeNull();
}); });
it('should display error message if error is != 400', () => { it('should display error message if error is 500', () => {
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 500 } }; (comp as any).searchResults = { hasFailed: true, statusCode: 500 };
fixture.detectChanges(); fixture.detectChanges();
expect(comp.showError()).toBeTrue();
expect(comp.errorMessageLabel()).toBe('error.search-results');
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
});
it('should display error message if error is 422', () => {
(comp as any).searchResults = { hasFailed: true, statusCode: 422 };
fixture.detectChanges();
expect(comp.showError()).toBeTrue();
expect(comp.errorMessageLabel()).toBe('error.invalid-search-query');
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull(); expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
}); });

View File

@@ -58,4 +58,12 @@ export class MyDSpaceResultsComponent {
isLoading() { isLoading() {
return !this.searchResults || isEmpty(this.searchResults) || this.searchResults.isLoading; return !this.searchResults || isEmpty(this.searchResults) || this.searchResults.isLoading;
} }
showError(): boolean {
return this.searchResults?.hasFailed && (!this.searchResults?.errorMessage || this.searchResults?.statusCode !== 400);
}
errorMessageLabel(): string {
return (this.searchResults?.statusCode === 422) ? 'error.invalid-search-query' : 'error.search-results';
}
} }

View File

@@ -50,7 +50,7 @@ const ENTRY_COMPONENTS = [
export class MyDspaceSearchModule { export class MyDspaceSearchModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -58,7 +58,7 @@ const ENTRY_COMPONENTS = [
export class NavbarModule { export class NavbarModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -8,7 +8,7 @@ import { pushInOut } from '../shared/animations/push';
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { SidebarService } from '../shared/sidebar/sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue, isEmpty } from '../shared/empty.util'; import { hasValue, isEmpty } from '../shared/empty.util';
import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { RouteService } from '../core/services/route.service'; import { RouteService } from '../core/services/route.service';
import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
@@ -126,8 +126,8 @@ export class SearchComponent implements OnInit {
this.searchOptions$ = this.getSearchOptions(); this.searchOptions$ = this.getSearchOptions();
this.sub = this.searchOptions$.pipe( this.sub = this.searchOptions$.pipe(
switchMap((options) => this.service.search( switchMap((options) => this.service.search(
options, undefined, true, true, followLink<Item>('thumbnail', { isOptional: true }) options, undefined, false, true, followLink<Item>('thumbnail', { isOptional: true })
).pipe(getFirstSucceededRemoteData(), startWith(undefined)) ).pipe(getFirstCompletedRemoteData(), startWith(undefined))
) )
).subscribe((results) => { ).subscribe((results) => {
this.resultsRD$.next(results); this.resultsRD$.next(results);

View File

@@ -1,3 +1,4 @@
<div> <ds-alert [type]="AlertTypeEnum.Error" [dismissible]="false">
<label>{{ message }}</label> <!-- Using [innerHTML] instead of {{message}} allows to render HTML code -->
</div> <span [innerHTML]="message"></span>
</ds-alert>

View File

@@ -36,7 +36,7 @@ describe('ErrorComponent (inline template)', () => {
comp = fixture.componentInstance; // ErrorComponent test instance comp = fixture.componentInstance; // ErrorComponent test instance
// query for the message <label> by CSS element selector // query for the message <label> by CSS element selector
de = fixture.debugElement.query(By.css('label')); de = fixture.debugElement.query(By.css('ds-alert'));
el = de.nativeElement; el = de.nativeElement;
}); });

View File

@@ -3,6 +3,7 @@ import { Component, Input } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AlertType } from '../alert/aletr-type';
@Component({ @Component({
selector: 'ds-error', selector: 'ds-error',
@@ -13,6 +14,12 @@ export class ErrorComponent {
@Input() message = 'Error...'; @Input() message = 'Error...';
/**
* The AlertType enumeration
* @type {AlertType}
*/
public AlertTypeEnum = AlertType;
private subscription: Subscription; private subscription: Subscription;
constructor(private translate: TranslateService) { constructor(private translate: TranslateService) {

View File

@@ -169,7 +169,7 @@ export class SearchFilterComponent implements OnInit {
return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe( return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe(
filter((RD) => !RD.isLoading), filter((RD) => !RD.isLoading),
map((valuesRD) => { map((valuesRD) => {
return valuesRD.payload.totalElements > 0; return valuesRD.payload?.totalElements > 0;
}),); }),);
} }
)); ));

View File

@@ -16,11 +16,11 @@
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-loading <ds-loading
*ngIf="hasNoValue(searchResults) || hasNoValue(searchResults.payload) || searchResults.isLoading" *ngIf="!showError() && (hasNoValue(searchResults) || hasNoValue(searchResults.payload) || searchResults.isLoading)"
message="{{'loading.search-results' | translate}}"></ds-loading> message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-error <ds-error
*ngIf="searchResults?.hasFailed && (!searchResults?.errorMessage || searchResults?.statusCode != 400)" *ngIf="showError()"
message="{{'error.search-results' | 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">
{{ 'search.results.no-results' | translate }} {{ 'search.results.no-results' | translate }}
<a [routerLink]="['/search']" <a [routerLink]="['/search']"

View File

@@ -45,9 +45,19 @@ describe('SearchResultsComponent', () => {
expect(fixture.debugElement.query(By.css('a'))).toBeNull(); expect(fixture.debugElement.query(By.css('a'))).toBeNull();
}); });
it('should display error message if error is != 400', () => { it('should display error message if error is 500', () => {
(comp as any).searchResults = createFailedRemoteDataObject('Error', 500); (comp as any).searchResults = createFailedRemoteDataObject('Error', 500);
fixture.detectChanges(); fixture.detectChanges();
expect(comp.showError()).toBeTrue();
expect(comp.errorMessageLabel()).toBe('error.search-results');
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
});
it('should display error message if error is 422', () => {
(comp as any).searchResults = createFailedRemoteDataObject('Error', 422);
fixture.detectChanges();
expect(comp.showError()).toBeTrue();
expect(comp.errorMessageLabel()).toBe('error.invalid-search-query');
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull(); expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
}); });

View File

@@ -78,6 +78,14 @@ export class SearchResultsComponent {
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
showError(): boolean {
return this.searchResults?.hasFailed && (!this.searchResults?.errorMessage || this.searchResults?.statusCode !== 400);
}
errorMessageLabel(): string {
return (this.searchResults?.statusCode === 422) ? 'error.invalid-search-query' : 'error.search-results';
}
/** /**
* Method to change the given string by surrounding it by quotes if not already present. * Method to change the given string by surrounding it by quotes if not already present.
*/ */

View File

@@ -623,7 +623,7 @@ const DIRECTIVES = [
export class SharedModule { export class SharedModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -65,6 +65,13 @@ const DECLARATIONS = [
SubmissionImportExternalCollectionComponent SubmissionImportExternalCollectionComponent
]; ];
const ENTRY_COMPONENTS = [
SubmissionSectionUploadComponent,
SubmissionSectionformComponent,
SubmissionSectionLicenseComponent,
SubmissionSectionCcLicensesComponent
];
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
@@ -88,4 +95,14 @@ const DECLARATIONS = [
* This module handles all components that are necessary for the submission process * This module handles all components that are necessary for the submission process
*/ */
export class SubmissionModule { export class SubmissionModule {
/**
* NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during SSR otherwise
*/
static withEntryComponents() {
return {
ngModule: SubmissionModule,
providers: ENTRY_COMPONENTS.map((component) => ({provide: component}))
};
}
} }

View File

@@ -1336,6 +1336,8 @@
"error.search-results": "Error fetching search results", "error.search-results": "Error fetching search results",
"error.invalid-search-query": "Search query is not valid. Please check <a href=\"https://solr.apache.org/guide/query-syntax-and-parsing.html\" target=\"_blank\">Solr query syntax</a> best practices for further information about this error.",
"error.sub-collections": "Error fetching sub-collections", "error.sub-collections": "Error fetching sub-collections",
"error.sub-communities": "Error fetching sub-communities", "error.sub-communities": "Error fetching sub-communities",