Merge branch 'main' of github.com:DSpace/dspace-angular into 1422-deploy-time-config

This commit is contained in:
William Welling
2021-12-20 12:30:53 -06:00
55 changed files with 343 additions and 92 deletions

View File

@@ -269,7 +269,9 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con
The test files can be found in the `./cypress/integration/` folder.
Before you can run e2e tests, you MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `config.prod.yml` or `config.yml`. You may override this using env variables, see [Configuring](#configuring).
Before you can run e2e tests, two things are required:
1. You MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `config.prod.yml` or `config.yml`. You may override this using env variables, see [Configuring](#configuring).
2. Your backend MUST include our Entities Test Data set. Some tests run against a (currently hardcoded) Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set. The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data
Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.

View File

@@ -5,5 +5,6 @@
"screenshotsFolder": "cypress/screenshots",
"pluginsFile": "cypress/plugins/index.ts",
"fixturesFolder": "cypress/fixtures",
"baseUrl": "http://localhost:4000"
"baseUrl": "http://localhost:4000",
"retries": 2
}

View File

@@ -64,7 +64,7 @@ services:
dspacesolr:
container_name: dspacesolr
# 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
depends_on:
- dspace

View File

@@ -62,7 +62,7 @@ services:
dspacesolr:
container_name: dspacesolr
# 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
depends_on:
- dspace

View File

@@ -97,9 +97,9 @@
"jwt-decode": "^3.1.2",
"klaro": "^0.7.10",
"lodash": "^4.17.21",
"mirador": "^3.0.0",
"mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.10.0",
"mirador-share-plugin": "^0.11.0",
"moment": "^2.29.1",
"morgan": "^1.10.0",
"ng-mocks": "11.11.2",
@@ -112,8 +112,6 @@
"nouislider": "^14.6.3",
"pem": "1.14.4",
"postcss-cli": "^8.3.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^6.6.3",
"sortablejs": "1.13.0",
@@ -175,6 +173,8 @@
"protractor": "^7.0.0",
"protractor-istanbul-plugin": "2.0.0",
"raw-loader": "0.5.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"rimraf": "^3.0.2",
"rxjs-spy": "^7.5.3",
"sass-resources-loader": "^2.1.1",

View File

@@ -203,7 +203,6 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
]}
],{
onSameUrlNavigation: 'reload',
relativeLinkResolution: 'legacy'
})
],
exports: [RouterModule],

View File

@@ -174,7 +174,8 @@ describe('App component', () => {
TestBed.configureTestingModule(getDefaultTestBedConf());
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
document = TestBed.inject(DOCUMENT);
headSpy = jasmine.createSpyObj('head', ['appendChild']);
headSpy = jasmine.createSpyObj('head', ['appendChild', 'getElementsByClassName']);
headSpy.getElementsByClassName.and.returnValue([]);
spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]);
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;

View File

@@ -34,12 +34,12 @@ import { AuthService } from './core/auth/auth.service';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service';
import { HostWindowService } from './shared/host-window.service';
import { ThemeConfig } from '../config/theme.model';
import { HeadTagConfig, ThemeConfig } from '../config/theme.model';
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
import { environment } from '../environments/environment';
import { models } from './core/core.module';
import { LocaleService } from './core/locale/locale.service';
import { hasValue, isNotEmpty } from './shared/empty.util';
import { hasNoValue, hasValue, isNotEmpty } from './shared/empty.util';
import { KlaroService } from './shared/cookies/klaro.service';
import { GoogleAnalyticsService } from './statistics/google-analytics.service';
import { ThemeService } from './shared/theme-support/theme.service';
@@ -124,13 +124,13 @@ export class AppComponent implements OnInit, AfterViewInit {
this.isThemeCSSLoading$.next(true);
}
if (hasValue(themeName)) {
this.setThemeCss(themeName);
this.loadGlobalThemeConfig(themeName);
} else {
const defaultThemeConfig = getDefaultThemeConfig();
if (hasValue(defaultThemeConfig)) {
this.setThemeCss(defaultThemeConfig.name);
this.loadGlobalThemeConfig(defaultThemeConfig.name);
} else {
this.setThemeCss(BASE_THEME_NAME);
this.loadGlobalThemeConfig(BASE_THEME_NAME);
}
}
});
@@ -245,6 +245,11 @@ export class AppComponent implements OnInit, AfterViewInit {
}
}
private loadGlobalThemeConfig(themeName: string): void {
this.setThemeCss(themeName);
this.setHeadTags(themeName);
}
/**
* Update the theme css file in <head>
*
@@ -253,9 +258,13 @@ export class AppComponent implements OnInit, AfterViewInit {
*/
private setThemeCss(themeName: string): void {
const head = this.document.getElementsByTagName('head')[0];
if (hasNoValue(head)) {
return;
}
// Array.from to ensure we end up with an array, not an HTMLCollection, which would be
// automatically updated if we add nodes later
const currentThemeLinks = Array.from(this.document.getElementsByClassName('theme-css'));
const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css'));
const link = this.document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
@@ -277,6 +286,78 @@ export class AppComponent implements OnInit, AfterViewInit {
head.appendChild(link);
}
private setHeadTags(themeName: string): void {
const head = this.document.getElementsByTagName('head')[0];
if (hasNoValue(head)) {
return;
}
// clear head tags
const currentHeadTags = Array.from(head.getElementsByClassName('theme-head-tag'));
if (hasValue(currentHeadTags)) {
currentHeadTags.forEach((currentHeadTag: any) => currentHeadTag.remove());
}
// create new head tags (not yet added to DOM)
const headTagFragment = this.document.createDocumentFragment();
this.createHeadTags(themeName)
.forEach(newHeadTag => headTagFragment.appendChild(newHeadTag));
// add new head tags to DOM
head.appendChild(headTagFragment);
}
private createHeadTags(themeName: string): HTMLElement[] {
const themeConfig = this.themeService.getThemeConfigFor(themeName);
const headTagConfigs = themeConfig?.headTags;
if (hasNoValue(headTagConfigs)) {
const parentThemeName = themeConfig?.extends;
if (hasValue(parentThemeName)) {
// inherit the head tags of the parent theme
return this.createHeadTags(parentThemeName);
}
const defaultThemeConfig = getDefaultThemeConfig();
const defaultThemeName = defaultThemeConfig.name;
if (
hasNoValue(defaultThemeName) ||
themeName === defaultThemeName ||
themeName === BASE_THEME_NAME
) {
// last resort, use fallback favicon.ico
return [
this.createHeadTag({
'tagName': 'link',
'attributes': {
'rel': 'icon',
'href': 'assets/images/favicon.ico',
'sizes': 'any',
}
})
];
}
// inherit the head tags of the default theme
return this.createHeadTags(defaultThemeConfig.name);
}
return headTagConfigs.map(this.createHeadTag.bind(this));
}
private createHeadTag(headTagConfig: HeadTagConfig): HTMLElement {
const tag = this.document.createElement(headTagConfig.tagName);
if (hasValue(headTagConfig.attributes)) {
Object.entries(headTagConfig.attributes)
.forEach(([key, value]) => tag.setAttribute(key, value));
}
// 'class' attribute should always be 'theme-head-tag' for removal
tag.setAttribute('class', 'theme-head-tag');
return tag;
}
private trackIdleModal() {
const isIdle$ = this.authService.isUserIdle();
const isAuthenticated$ = this.authService.isAuthenticated();

View File

@@ -10,11 +10,11 @@
</nav>
<ng-template #breadcrumb let-text="text" let-url="url">
<li class="breadcrumb-item"><a [routerLink]="url">{{text | translate}}</a></li>
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate">{{text | translate}}</a></div></li>
</ng-template>
<ng-template #activeBreadcrumb let-text="text">
<li class="breadcrumb-item active" aria-current="page">{{text | translate}}</li>
<li class="breadcrumb-item active" aria-current="page"><div class="breadcrumb-item-limiter"><div class="text-truncate">{{text | translate}}</div></div></li>
</ng-template>
</ng-container>

View File

@@ -10,6 +10,19 @@
background-color: var(--ds-breadcrumb-bg);
}
li.breadcrumb-item {
display: flex;
}
.breadcrumb-item-limiter {
display: inline-block;
max-width: var(--ds-breadcrumb-max-length);
> * {
max-width: 100%;
display: block;
}
}
li.breadcrumb-item > a {
color: var(--ds-breadcrumb-link-color) !important;
}
@@ -18,5 +31,6 @@ li.breadcrumb-item.active {
}
.breadcrumb-item+ .breadcrumb-item::before {
display: block;
content: quote("") !important;
}

View File

@@ -72,7 +72,7 @@ describe('BreadcrumbsComponent', () => {
expect(breadcrumbs.length).toBe(3);
expectBreadcrumb(breadcrumbs[0], 'home.breadcrumbs', '/');
expectBreadcrumb(breadcrumbs[1], 'bc 1', '/example.com');
expectBreadcrumb(breadcrumbs[2], 'bc 2', null);
expectBreadcrumb(breadcrumbs[2].query(By.css('.text-truncate')), 'bc 2', null);
});
});

View File

@@ -1,10 +1,10 @@
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead item-list-title"
class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span>
<span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1">

View File

@@ -1,10 +1,10 @@
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead item-list-title"
class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span>
<span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1">

View File

@@ -1,10 +1,10 @@
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead item-list-title"
class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span>
<span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1">

View File

@@ -1,10 +1,10 @@
<ds-truncatable [id]="dso.id">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None"
class="lead item-list-title"
class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span>
<!--<span class="text-muted">-->
<!--<ds-truncatable-part [id]="dso.id" [minLines]="1">-->

View File

@@ -1,18 +1,24 @@
<ng-template #bitstreamView>
<div class="{{columnSizes.columns[0].buildClasses()}} row-element d-flex">
<ng-content select="[slot=drag-handle]"></ng-content>
<div class="float-left d-flex align-items-center">
<div class="float-left d-flex align-items-center overflow-hidden">
<span class="text-truncate">
{{ bitstreamName }}
</span>
</div>
</div>
<div class="{{columnSizes.columns[1].buildClasses()}} row-element d-flex align-items-center">
<div class="w-100">
<span class="text-truncate">
{{ bitstream?.firstMetadataValue('dc.description') }}
</span>
</div>
</div>
<div class="{{columnSizes.columns[2].buildClasses()}} row-element d-flex align-items-center">
<div class="text-center w-100">
<span class="text-truncate">
{{ (format$ | async)?.shortDescription }}
</span>
</div>
</div>
<div class="{{columnSizes.columns[3].buildClasses()}} row-element d-flex align-items-center">

View File

@@ -26,7 +26,7 @@
<td class="w-100">
<div class="value-field">
<div *ngIf="!(editable | async)">
<span>{{metadata?.value}}</span>
<span class="dont-break-out">{{metadata?.value}}</span>
</div>
<div *ngIf="(editable | async)" class="field-container">
<textarea class="form-control" type="textarea" attr.aria-labelledby="fieldValue" [(ngModel)]="metadata.value" [dsDebounce]

View File

@@ -1,5 +1,5 @@
<ds-metadata-field-wrapper [label]="label | translate">
<a *ngFor="let mdValue of mdValues; let last=last;" [href]="mdValue.value">
<a class="dont-break-out" *ngFor="let mdValue of mdValues; let last=last;" [href]="mdValue.value">
{{ linktext || mdValue.value }}<span *ngIf="!last" [innerHTML]="separator"></span>
</a>
</ds-metadata-field-wrapper>

View File

@@ -1,5 +1,5 @@
<ds-metadata-field-wrapper [label]="label | translate">
<span *ngFor="let mdValue of mdValues; let last=last;">
<span class="dont-break-out" *ngFor="let mdValue of mdValues; let last=last;">
{{mdValue.value}}<span *ngIf="!last" [innerHTML]="separator"></span>
</span>
</ds-metadata-field-wrapper>

View File

@@ -4,7 +4,7 @@
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
id="search-sidebar"
[configurationList]="(configurationList$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
[viewModeList]="viewModeList"
[searchOptions]="(searchOptions$ | async)"
[sortOptions]="(sortOptions$ | async)"
@@ -27,7 +27,7 @@
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
id="search-sidebar-sm"
[configurationList]="(configurationList$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
(toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | 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 { SidebarService } from '../shared/sidebar/sidebar.service';
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 { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
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 { Context } from '../core/shared/context.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 SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
@@ -111,8 +111,7 @@ export class MyDSpacePageComponent implements OnInit {
constructor(private service: SearchService,
private sidebarService: SidebarService,
private windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService,
private routeService: RouteService) {
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
}
@@ -134,8 +133,8 @@ export class MyDSpacePageComponent implements OnInit {
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
this.sub = this.searchOptions$.pipe(
tap(() => this.resultsRD$.next(null)),
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstSucceededRemoteData())))
.subscribe((results) => {
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstCompletedRemoteData())))
.subscribe((results: RemoteData<SearchObjects<DSpaceObject>>) => {
this.resultsRD$.next(results);
});

View File

@@ -10,5 +10,5 @@
</ds-viewable-collection>
</div>
<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>

View File

@@ -40,9 +40,19 @@ describe('MyDSpaceResultsComponent', () => {
expect(fixture.debugElement.query(By.css('a'))).toBeNull();
});
it('should display error message if error is != 400', () => {
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 500 } };
it('should display error message if error is 500', () => {
(comp as any).searchResults = { hasFailed: true, statusCode: 500 };
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();
});

View File

@@ -58,4 +58,12 @@ export class MyDSpaceResultsComponent {
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

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

View File

@@ -11,8 +11,8 @@
aria-labelledby="dropdownMenuButton"
(scroll)="onScroll($event)"
infiniteScroll
[infiniteScrollDistance]="5"
[infiniteScrollThrottle]="300"
[infiniteScrollDistance]="1.5"
[infiniteScrollThrottle]="0"
[infiniteScrollUpDistance]="1.5"
[fromRoot]="true"
[scrollWindow]="false"
@@ -21,7 +21,7 @@
<button class="dropdown-item disabled" *ngIf="searchListCollection?.length == 0 && !(isLoading | async)">
{{'submission.sections.general.no-collection' | translate}}
</button>
<ng-container *ngIf="searchListCollection?.length > 0 && !(isLoading | async)">
<ng-container *ngIf="searchListCollection?.length > 0">
<button *ngFor="let listItem of searchListCollection"
class="dropdown-item collection-item"
title="{{ listItem.collection.name }}"

View File

@@ -223,8 +223,9 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
switchMap((collectionsRD: RemoteData<PaginatedList<Collection>>) => {
this.searchComplete.emit();
if (collectionsRD.hasSucceeded && collectionsRD.payload.totalElements > 0) {
if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collectionsRD.payload.totalElements ) {
if (this.searchListCollection.length >= collectionsRD.payload.totalElements) {
this.hasNextPage = false;
}
this.emitSelectionEvents(collectionsRD);
return observableFrom(collectionsRD.payload.page).pipe(
mergeMap((collection: Collection) => collection.parentCommunity.pipe(
@@ -236,7 +237,6 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
))),
reduce((acc: any, value: any) => [...acc, value], []),
);
}
} else {
this.hasNextPage = false;
return observableOf([]);

View File

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

View File

@@ -36,7 +36,7 @@ describe('ErrorComponent (inline template)', () => {
comp = fixture.componentInstance; // ErrorComponent test instance
// 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;
});

View File

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

View File

@@ -1,4 +1,4 @@
<a [routerLink]="(bitstreamPath$| async)?.routerLink" [queryParams]="(bitstreamPath$| async)?.queryParams" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
<a [routerLink]="(bitstreamPath$| async)?.routerLink" class="dont-break-out" [queryParams]="(bitstreamPath$| async)?.queryParams" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
<span *ngIf="!(canDownload$ |async)"><i class="fas fa-lock"></i></span>
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>

View File

@@ -1,3 +1,3 @@
<div>
<span>{{metadataRepresentation.getValue()}}</span>
<span class="dont-break-out">{{metadataRepresentation.getValue()}}</span>
</div>

View File

@@ -2,9 +2,9 @@
<ds-truncatable [id]="dso.id" *ngIf="object !== undefined && object !== null">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None" class="lead item-list-title"
<span *ngIf="linkType == linkTypes.None" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span>
<span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1">

View File

@@ -281,7 +281,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
* Redirect to resource policy creation page
*/
redirectToResourcePolicyCreatePage(): void {
this.router.navigate([`../create`], {
this.router.navigate([`./create`], {
relativeTo: this.route,
queryParams: {
policyTargetId: this.resourceUUID,
@@ -296,7 +296,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
* @param policy The resource policy
*/
redirectToResourcePolicyEditPage(policy: ResourcePolicy): void {
this.router.navigate([`../edit`], {
this.router.navigate([`./edit`], {
relativeTo: this.route,
queryParams: {
policyId: policy.id

View File

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

View File

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

View File

@@ -45,9 +45,19 @@ describe('SearchResultsComponent', () => {
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);
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();
});

View File

@@ -78,6 +78,14 @@ export class SearchResultsComponent {
@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.
*/

View File

@@ -1,5 +1,5 @@
<div class="clamp-{{background}}-{{lines}} min-{{minLines}} {{type}} {{fixedHeight ? 'fixedHeight' : ''}}">
<div class="content">
<div class="content dont-break-out">
<ng-content></ng-content>
</div>
</div>

View File

@@ -2182,7 +2182,7 @@
"item.edit.bitstreams.headers.name": "Name",
// "item.edit.bitstreams.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button",
"item.edit.bitstreams.notifications.discarded.content": "Ihre Änderungen wurden verworfen. Um Ihre Änderungen wiederherzustellen, klicken Sie auf die Schaltfläche 'Rückgängig',
"item.edit.bitstreams.notifications.discarded.content": "Ihre Änderungen wurden verworfen. Um Ihre Änderungen wiederherzustellen, klicken Sie auf die Schaltfläche 'Rückgängig'",
// "item.edit.bitstreams.notifications.discarded.title": "Changes discarded",
"item.edit.bitstreams.notifications.discarded.title": "Änderungen verworfen",

View File

@@ -1331,6 +1331,8 @@
"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-communities": "Error fetching sub-communities",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -292,11 +292,48 @@ export class DefaultAppConfig implements AppConfig {
{
// The default dspace theme
name: 'dspace'
name: 'dspace',
// Whenever this theme is active, the following tags will be injected into the <head> of the page.
// Example use case: set the favicon based on the active theme.
headTags: [
{
// Insert <link rel="icon" href="assets/dspace/images/favicons/favicon.ico" sizes="any"/> into the <head> of the page.
tagName: 'link',
attributes: {
'rel': 'icon',
'href': 'assets/dspace/images/favicons/favicon.ico',
'sizes': 'any',
}
},
{
// Insert <link rel="icon" href="assets/dspace/images/favicons/favicon.svg" type="image/svg+xml"/> into the <head> of the page.
tagName: 'link',
attributes: {
'rel': 'icon',
'href': 'assets/dspace/images/favicons/favicon.svg',
'type': 'image/svg+xml',
}
},
{
// Insert <link rel="apple-touch-icon" href="assets/dspace/images/favicons/apple-touch-icon.png"/> into the <head> of the page.
tagName: 'link',
attributes: {
'rel': 'apple-touch-icon',
'href': 'assets/dspace/images/favicons/apple-touch-icon.png',
}
},
{
// Insert <link rel="manifest" href="assets/dspace/images/favicons/manifest.webmanifest"/> into the <head> of the page.
tagName: 'link',
attributes: {
'rel': 'manifest',
'href': 'assets/dspace/images/favicons/manifest.webmanifest',
}
},
]
},
];
// Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video').
// Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with "image" or "video").
// For images, this enables a gallery viewer where you can zoom or page through images.
// For videos, this enables embedded video streaming
mediaViewer: MediaViewerConfig = {

View File

@@ -12,6 +12,28 @@ export interface NamedThemeConfig extends Config {
* its ancestor theme(s) will be checked recursively before falling back to the default theme.
*/
extends?: string;
/**
* A list of HTML tags that should be added to the HEAD section of the document, whenever this theme is active.
*/
headTags?: HeadTagConfig[];
}
/**
* Interface that represents a single theme-specific HTML tag in the HEAD section of the page.
*/
export interface HeadTagConfig extends Config {
/**
* The name of the HTML tag
*/
tagName: string;
/**
* The attributes on the HTML tag
*/
attributes?: {
[key: string]: string;
};
}
export interface RegExThemeConfig extends NamedThemeConfig {

View File

@@ -6,7 +6,6 @@
<base href="/">
<title>DSpace</title>
<meta name="viewport" content="width=device-width,minimum-scale=1">
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" />
</head>
<body>

View File

@@ -6,7 +6,6 @@
<base href="/">
<title>DSpace</title>
<meta name="viewport" content="width=device-width,minimum-scale=1">
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" />
</head>
<body>

View File

@@ -79,6 +79,7 @@
--ds-breadcrumb-bg: #{$gray-200} !important;
--ds-breadcrumb-link-color: #{$cyan};
--ds-breadcrumb-link-active-color: #{darken($cyan, 30%)};
--ds-breadcrumb-max-length: 200px;
--ds-slider-color: #{$green};
--ds-slider-handle-width: 18px;

View File

@@ -74,3 +74,21 @@ ngb-modal-backdrop {
}
}
.dont-break-out {
/* These are technically the same, but use both */
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
/* This is the dangerous one in WebKit, as it breaks things wherever */
word-break: break-all;
/* Instead use this non-standard one: */
word-break: break-word;
/* Adds a hyphen where the word breaks, if supported (No Blink) */
-ms-hyphens: auto;
-moz-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 98 98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0.019,2.867)">
<path d="M53.561,58.569L53.67,58.563L53.786,58.553L53.892,58.54L54.002,58.53L54.112,58.507L54.221,58.488L54.327,58.465L54.433,58.436L54.538,58.413L54.644,58.38L54.747,58.346L54.844,58.311L54.948,58.271L55.049,58.229L55.149,58.187L55.149,58.186L55.245,58.141L55.34,58.097L55.437,58.048L55.528,57.992L55.528,57.991L55.622,57.941L55.622,57.939L55.712,57.883L55.712,57.882L55.805,57.822L55.888,57.766L55.888,57.765L55.973,57.702L56.061,57.637L56.061,57.635L56.148,57.573L56.148,57.572C56.964,56.889 57.541,55.926 57.709,54.834L57.722,54.73L57.722,54.72L57.736,54.619L57.736,54.608L57.745,54.51L57.745,54.498L57.754,54.397L57.754,54.38L57.759,54.285L57.759,54.269L57.761,54.164L57.761,37.704L57.759,37.599L57.759,37.583L57.754,37.488L57.754,37.475L57.745,37.374L57.745,37.362L57.736,37.264L57.736,37.253L57.722,37.153L57.722,37.143L57.709,37.039C57.542,35.947 56.965,34.982 56.148,34.301L56.148,34.3L56.061,34.237L56.061,34.235L55.973,34.17L55.888,34.107L55.888,34.106L55.805,34.05L55.712,33.989L55.622,33.933L55.622,33.93L55.528,33.88L55.528,33.879L55.437,33.823L55.34,33.774L55.245,33.731L55.245,33.73L55.149,33.685L55.149,33.684L55.049,33.641L54.948,33.599L54.844,33.559L54.747,33.524L54.644,33.493L54.538,33.457L54.433,33.434L54.327,33.406L54.221,33.382L54.112,33.363L54.002,33.34L53.892,33.331L53.786,33.317L53.67,33.307L53.561,33.301L53.447,33.296L45.557,33.296C35.841,33.296 29.699,25.458 29.699,16.146L29.699,6.92C29.699,3.108 26.597,0.005 22.785,0.005L6.92,0.005C3.107,0.005 -0,3.111 -0,6.92L-0,23.602C-0,27.408 3.104,30.511 6.92,30.511L15.334,30.511C24.503,30.511 32.24,36.461 32.48,45.914L32.48,45.954C32.24,55.407 24.502,61.356 15.334,61.356L6.92,61.356C3.105,61.356 -0,64.459 -0,68.265L-0,84.947C-0,88.757 3.106,91.862 6.92,91.862L22.785,91.862C26.597,91.862 29.699,88.758 29.699,84.947L29.699,75.724C29.699,66.412 35.843,58.575 45.557,58.575L53.447,58.575L53.561,58.569ZM87.607,9.956C81.466,3.814 72.985,0 63.651,0L48.627,0L48.627,17.424L63.651,17.424C68.177,17.424 72.298,19.282 75.291,22.273C78.281,25.263 80.14,29.385 80.14,33.912L80.14,57.954C80.14,62.492 78.287,66.619 75.308,69.609L75.291,69.593C72.3,72.584 68.178,74.442 63.651,74.442L48.627,74.442L48.627,91.866L63.651,91.866C72.984,91.866 81.465,88.052 87.607,81.91L87.607,81.877C93.749,75.734 97.562,67.263 97.562,57.954L97.562,33.912C97.562,24.578 93.749,16.097 87.607,9.956Z" style="fill:rgb(146,198,66);fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,19 @@
{
"name": "DSpace",
"short_name": "DSpace",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#091119",
"background_color": "#091119",
"display": "standalone"
}

View File

@@ -9111,12 +9111,12 @@ mirador-dl-plugin@^0.13.0:
resolved "https://registry.yarnpkg.com/mirador-dl-plugin/-/mirador-dl-plugin-0.13.0.tgz#9a6cb0fa3c566a2a1ebe1ad9caa1ff590ff22689"
integrity sha512-I/6etIvpTtO1zgjxx2uEUFoyB9NxQ43JWg8CMkKmZqblW7AAeFqRn1/zUlQH7N8KFZft9Rah6D8qxtuNAo9jmA==
mirador-share-plugin@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/mirador-share-plugin/-/mirador-share-plugin-0.10.0.tgz#82cde27faedc440fab648db137e62849d6542420"
integrity sha512-hC9hG0H04WAR6JNfLDnQICtxwWV3K+cmqnArtOvAIGGnbgXWs5tmQyfdY55z05jzbeL40rd7z1K094hHV3R4WQ==
mirador-share-plugin@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/mirador-share-plugin/-/mirador-share-plugin-0.11.0.tgz#13e2f654e38839044382acad42d9329e91a8cd5e"
integrity sha512-fHcdDXyrtfy5pn1zdQNX9BvE5Tjup66eQwyNippE5PMaP8ImUcrFaSL+mStdn+v6agsHcsdRqLhseZ0XWgEuAw==
mirador@^3.0.0:
mirador@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/mirador/-/mirador-3.3.0.tgz#7a957a1db1a5388b2b8cafab00db4eb9f97557b9"
integrity sha512-BmGfRnWJ45B+vtiAwcFT7n9nKialfejE9UvuUK0NorO37ShArpsKr3yVSD4jQASwSR4DRRpPEG21jOk4WN7H3w==