diff --git a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html index c791cec600a..d69f87883bb 100644 --- a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html +++ b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html @@ -1,4 +1,4 @@ -
+
{{ label }}
diff --git a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts index 9c62b80cad7..e17e5397b7e 100644 --- a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts +++ b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component'; @@ -6,35 +6,34 @@ import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.componen /* tslint:disable:max-classes-per-file */ @Component({ selector: 'ds-component-without-content', - template: '\n' + + template: '\n' + '' }) -class NoContentComponent {} +class NoContentComponent { + public hideIfNoTextContent = true; +} @Component({ selector: 'ds-component-with-empty-spans', - template: '\n' + + template: '\n' + ' \n' + ' \n' + '' }) -class SpanContentComponent {} +class SpanContentComponent { + @Input() hideIfNoTextContent = true; +} @Component({ selector: 'ds-component-with-text', - template: '\n' + + template: '\n' + ' The quick brown fox jumps over the lazy dog\n' + '' }) -class TextContentComponent {} +class TextContentComponent { + @Input() hideIfNoTextContent = true; +} -@Component({ - selector: 'ds-component-with-image', - template: '\n' + - ' an alt text\n' + - '' -}) -class ImgContentComponent {} /* tslint:enable:max-classes-per-file */ describe('MetadataFieldWrapperComponent', () => { @@ -43,7 +42,7 @@ describe('MetadataFieldWrapperComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent, ImgContentComponent] + declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent] }).compileComponents(); })); @@ -58,38 +57,60 @@ describe('MetadataFieldWrapperComponent', () => { expect(component).toBeDefined(); }); - it('should not show the component when there is no content', () => { - const parentFixture = TestBed.createComponent(NoContentComponent); - parentFixture.detectChanges(); - const parentNative = parentFixture.nativeElement; - const nativeWrapper = parentNative.querySelector(wrapperSelector); - expect(nativeWrapper.classList.contains('d-none')).toBe(true); - }); + describe('with hideIfNoTextContent=true', () => { + it('should not show the component when there is no content', () => { + const parentFixture = TestBed.createComponent(NoContentComponent); + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + expect(nativeWrapper.classList.contains('d-none')).toBe(true); + }); - it('should not show the component when there is DOM content but not text or an image', () => { - const parentFixture = TestBed.createComponent(SpanContentComponent); - parentFixture.detectChanges(); - const parentNative = parentFixture.nativeElement; - const nativeWrapper = parentNative.querySelector(wrapperSelector); - expect(nativeWrapper.classList.contains('d-none')).toBe(true); - }); + it('should not show the component when there is no text content', () => { + const parentFixture = TestBed.createComponent(SpanContentComponent); + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + expect(nativeWrapper.classList.contains('d-none')).toBe(true); + }); - it('should show the component when there is text content', () => { - const parentFixture = TestBed.createComponent(TextContentComponent); - parentFixture.detectChanges(); - const parentNative = parentFixture.nativeElement; - const nativeWrapper = parentNative.querySelector(wrapperSelector); - parentFixture.detectChanges(); - expect(nativeWrapper.classList.contains('d-none')).toBe(false); + it('should show the component when there is text content', () => { + const parentFixture = TestBed.createComponent(TextContentComponent); + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + parentFixture.detectChanges(); + expect(nativeWrapper.classList.contains('d-none')).toBe(false); + }); }); - it('should show the component when there is img content', () => { - const parentFixture = TestBed.createComponent(ImgContentComponent); - parentFixture.detectChanges(); - const parentNative = parentFixture.nativeElement; - const nativeWrapper = parentNative.querySelector(wrapperSelector); - parentFixture.detectChanges(); - expect(nativeWrapper.classList.contains('d-none')).toBe(false); - }); + describe('with hideIfNoTextContent=false', () => { + it('should show the component when there is no content', () => { + const parentFixture = TestBed.createComponent(NoContentComponent); + parentFixture.componentInstance.hideIfNoTextContent = false; + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + expect(nativeWrapper.classList.contains('d-none')).toBe(false); + }); + it('should show the component when there is no text content', () => { + const parentFixture = TestBed.createComponent(SpanContentComponent); + parentFixture.componentInstance.hideIfNoTextContent = false; + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + expect(nativeWrapper.classList.contains('d-none')).toBe(false); + }); + + it('should show the component when there is text content', () => { + const parentFixture = TestBed.createComponent(TextContentComponent); + parentFixture.componentInstance.hideIfNoTextContent = false; + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + parentFixture.detectChanges(); + expect(nativeWrapper.classList.contains('d-none')).toBe(false); + }); + }); }); diff --git a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts index 8af108cceb8..5c6b99248f7 100644 --- a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts +++ b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts @@ -1,5 +1,4 @@ import { Component, Input } from '@angular/core'; -import { hasNoValue } from '../../../shared/empty.util'; /** * This component renders any content inside this wrapper. @@ -17,10 +16,5 @@ export class MetadataFieldWrapperComponent { */ @Input() label: string; - /** - * Make hasNoValue() available in the template - */ - hasNoValue(o: any): boolean { - return hasNoValue(o); - } + @Input() hideIfNoTextContent = true; } diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html index bc1c63cc322..c5393055dfc 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html @@ -11,7 +11,7 @@
{{"item.page.filesection.original.bundle" [retainScrollPosition]="true"> -
+
diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html index 73219cbb8f4..57b460c8144 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.html +++ b/src/app/+item-page/simple/item-types/publication/publication.component.html @@ -9,8 +9,8 @@

- - + + diff --git a/src/app/+item-page/simple/item-types/shared/item.component.ts b/src/app/+item-page/simple/item-types/shared/item.component.ts index 120eda930f7..130f67edc71 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.ts @@ -1,11 +1,12 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; import { environment } from '../../../../../environments/environment'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; -import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; +import { takeUntilCompletedRemoteData } from '../../../../core/shared/operators'; import { getItemPageRoute } from '../../../item-page-routing-paths'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { RemoteData } from '../../../../core/data/remote-data'; @Component({ selector: 'ds-item', @@ -17,6 +18,11 @@ import { getItemPageRoute } from '../../../item-page-routing-paths'; export class ItemComponent implements OnInit { @Input() object: Item; + /** + * The Item's thumbnail + */ + thumbnail$: BehaviorSubject>; + /** * Route to the item page */ @@ -28,12 +34,12 @@ export class ItemComponent implements OnInit { ngOnInit(): void { this.itemPageRoute = getItemPageRoute(this.object); - } - // TODO refactor to return RemoteData, and thumbnail template to deal with loading - getThumbnail(): Observable { - return this.bitstreamDataService.getThumbnailFor(this.object).pipe( - getFirstSucceededRemoteDataPayload() - ); + this.thumbnail$ = new BehaviorSubject>(undefined); + this.bitstreamDataService.getThumbnailFor(this.object).pipe( + takeUntilCompletedRemoteData(), + ).subscribe((rd: RemoteData) => { + this.thumbnail$.next(rd); + }); } } diff --git a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html index 8d46a2c5a9f..7ae9a1a9097 100644 --- a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -9,8 +9,8 @@

- - + + diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html index df6c9e60c03..feb282d3a7f 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html index cdc19b7f14d..aa2352b284d 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html index bacd657663d..8fdad598277 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index 5749c5797d6..8e357140d86 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -8,8 +8,8 @@

- - + +
- - + +
- - + +
- - + +
- - + +
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html index 005fa9cc831..23de8b134fa 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html index e84e8c49d06..88498a4d677 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index 822d4858ce6..d328e93b153 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -8,8 +8,13 @@

- - + + +
- - + + +
- - + + + diff --git a/src/app/shared/mocks/translate.service.mock.ts b/src/app/shared/mocks/translate.service.mock.ts index 0bc172b408d..38b088e50f3 100644 --- a/src/app/shared/mocks/translate.service.mock.ts +++ b/src/app/shared/mocks/translate.service.mock.ts @@ -3,6 +3,7 @@ import { TranslateService } from '@ngx-translate/core'; export function getMockTranslateService(): TranslateService { return jasmine.createSpyObj('translateService', { get: jasmine.createSpy('get'), + use: jasmine.createSpy('use'), instant: jasmine.createSpy('instant'), setDefaultLang: jasmine.createSpy('setDefaultLang') }); diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html index cbc3f9ccfb6..e4d2526eb2f 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html @@ -9,7 +9,7 @@

- + diff --git a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html index 9b9d1747040..d47e897edcc 100644 --- a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html +++ b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html @@ -1,11 +1,11 @@
- - + + - - + +

{{object.name}}

diff --git a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html index f676ba303b9..63097c4f579 100644 --- a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html +++ b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html @@ -1,11 +1,11 @@
- - + + - - + +

{{object.name}}

diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html deleted file mode 100644 index 1df4026f839..00000000000 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.scss b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.scss deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts deleted file mode 100644 index 825a4d5c60a..00000000000 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { Bitstream } from '../../../core/shared/bitstream.model'; -import { SafeUrlPipe } from '../../utils/safe-url-pipe'; - -import { GridThumbnailComponent } from './grid-thumbnail.component'; - -describe('GridThumbnailComponent', () => { - let comp: GridThumbnailComponent; - let fixture: ComponentFixture; - let de: DebugElement; - let el: HTMLElement; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [GridThumbnailComponent, SafeUrlPipe] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(GridThumbnailComponent); - comp = fixture.componentInstance; // BannerComponent test instance - de = fixture.debugElement.query(By.css('div.thumbnail')); - el = de.nativeElement; - }); - - it('should display image', () => { - const thumbnail = new Bitstream(); - thumbnail._links = { - self: { href: 'self.url' }, - bundle: { href: 'bundle.url' }, - format: { href: 'format.url' }, - content: { href: 'content.url' }, - }; - comp.thumbnail = thumbnail; - fixture.detectChanges(); - const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href); - }); - - it('should display placeholder', () => { - const thumbnail = new Bitstream(); - comp.thumbnail = thumbnail; - fixture.detectChanges(); - const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.defaultImage); - }); - -}); diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts deleted file mode 100644 index 92d93686dc2..00000000000 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - Component, - Input, - OnChanges, - OnInit, - SimpleChanges, -} from '@angular/core'; -import { Bitstream } from '../../../core/shared/bitstream.model'; -import { hasValue } from '../../empty.util'; - -/** - * This component renders a given Bitstream as a thumbnail. - * One input parameter of type Bitstream is expected. - * If no Bitstream is provided, a holderjs image will be rendered instead. - */ - -@Component({ - selector: 'ds-grid-thumbnail', - styleUrls: ['./grid-thumbnail.component.scss'], - templateUrl: './grid-thumbnail.component.html', -}) -export class GridThumbnailComponent implements OnInit, OnChanges { - @Input() thumbnail: Bitstream; - - data: any = {}; - - /** - * The default 'holder.js' image - */ - @Input() defaultImage? = - 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjYwIiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI2MCAxODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjwhLS0KU291cmNlIFVSTDogaG9sZGVyLmpzLzEwMCV4MTgwL3RleHQ6Tm8gVGh1bWJuYWlsCkNyZWF0ZWQgd2l0aCBIb2xkZXIuanMgMi42LjAuCkxlYXJuIG1vcmUgYXQgaHR0cDovL2hvbGRlcmpzLmNvbQooYykgMjAxMi0yMDE1IEl2YW4gTWFsb3BpbnNreSAtIGh0dHA6Ly9pbXNreS5jbwotLT48ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWyNob2xkZXJfMTVmNzJmMmFlMGIgdGV4dCB7IGZpbGw6I0FBQUFBQTtmb250LXdlaWdodDpib2xkO2ZvbnQtZmFtaWx5OkFyaWFsLCBIZWx2ZXRpY2EsIE9wZW4gU2Fucywgc2Fucy1zZXJpZiwgbW9ub3NwYWNlO2ZvbnQtc2l6ZToxM3B0IH0gXV0+PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNWY3MmYyYWUwYiI+PHJlY3Qgd2lkdGg9IjI2MCIgaGVpZ2h0PSIxODAiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSI3Mi4yNDIxODc1IiB5PSI5NiI+Tm8gVGh1bWJuYWlsPC90ZXh0PjwvZz48L2c+PC9zdmc+'; - - src: string; - - errorHandler(event) { - event.currentTarget.src = this.defaultImage; - } - - /** - * Initialize the src - */ - ngOnInit(): void { - this.src = this.defaultImage; - - this.checkThumbnail(this.thumbnail); - } - - /** - * If the old input is undefined and the new one is a bitsream then set src - */ - ngOnChanges(changes: SimpleChanges): void { - if ( - !hasValue(changes.thumbnail.previousValue) && - hasValue(changes.thumbnail.currentValue) - ) { - this.checkThumbnail(changes.thumbnail.currentValue); - } - } - - /** - * check if the Bitstream has any content than set the src - */ - checkThumbnail(thumbnail: Bitstream) { - if ( - hasValue(thumbnail) && - hasValue(thumbnail._links) && - thumbnail._links.content.href - ) { - this.src = thumbnail._links.content.href; - } - } -} diff --git a/src/app/shared/object-grid/object-grid.component.scss b/src/app/shared/object-grid/object-grid.component.scss index 46675615f0c..68a7f2f9913 100644 --- a/src/app/shared/object-grid/object-grid.component.scss +++ b/src/app/shared/object-grid/object-grid.component.scss @@ -1,7 +1,7 @@ :host ::ng-deep { --ds-wrapper-grid-spacing: calc(var(--bs-spacer) / 2); - div.thumbnail > img { + div.thumbnail > .thumbnail-content { height: var(--ds-card-thumbnail-height); width: 100%; display: block; diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html index f8c75fc0d4f..739fa6c7a80 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html @@ -1,11 +1,11 @@
- - + + - - + +
diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html index 8025213b3bc..d8c253c8a9c 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html @@ -1,11 +1,11 @@
- - + + - - + +
diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html index 85aeb63a6b3..bc168537219 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html @@ -6,14 +6,14 @@
- - + +
- - + +
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 4de0f2901ef..c5a91bd02cc 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -46,7 +46,6 @@ import { ThumbnailComponent } from '../thumbnail/thumbnail.component'; import { SearchFormComponent } from './search-form/search-form.component'; import { SearchResultGridElementComponent } from './object-grid/search-result-grid-element/search-result-grid-element.component'; import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component'; -import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component'; import { VarDirective } from './utils/var.directive'; import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component'; import { LogOutComponent } from './log-out/log-out.component'; @@ -54,8 +53,7 @@ import { FormComponent } from './form/form.component'; import { DsDynamicOneboxComponent } from './form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { - DsDynamicFormControlContainerComponent, - dsDynamicFormControlMapFn + DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn, } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component'; import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component'; import { DragClickDirective } from './utils/drag-click.directive'; @@ -340,7 +338,6 @@ const COMPONENTS = [ SidebarFilterComponent, SidebarFilterSelectedOptionComponent, ThumbnailComponent, - GridThumbnailComponent, UploaderComponent, FileDropzoneNoUploaderComponent, ItemListPreviewComponent, diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index dbf8f6732cf..bf70928392f 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,4 +1,14 @@ -
- +
+ + text-content + + + +
+
+
{{ placeholder | translate }}
+
+
+
- diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss index e2718bac063..b15238afac3 100644 --- a/src/app/thumbnail/thumbnail.component.scss +++ b/src/app/thumbnail/thumbnail.component.scss @@ -1,3 +1,35 @@ +.limit-width { + max-width: var(--ds-thumbnail-max-width); +} + img { max-width: 100%; } + +.outer { // .outer/.inner generated ~ https://ratiobuddy.com/ + position: relative; + &:before { + display: block; + content: ""; + width: 100%; + padding-top: (297 / 210) * 100%; // A4 ratio + } + > .inner { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + > .thumbnail-placeholder { + background: var(--ds-thumbnail-placeholder-background); + border: var(--ds-thumbnail-placeholder-border); + color: var(--ds-thumbnail-placeholder-color); + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + } + } +} diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index 21678c9162b..bc9d1597501 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -1,10 +1,22 @@ -import { DebugElement } from '@angular/core'; +import { DebugElement, Pipe, PipeTransform } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Bitstream } from '../core/shared/bitstream.model'; import { SafeUrlPipe } from '../shared/utils/safe-url-pipe'; -import { THUMBNAIL_PLACEHOLDER, ThumbnailComponent } from './thumbnail.component'; +import { ThumbnailComponent } from './thumbnail.component'; +import { RemoteData } from '../core/data/remote-data'; +import { + createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject, +} from '../shared/remote-data.utils'; + +// tslint:disable-next-line:pipe-prefix +@Pipe({ name: 'translate' }) +class MockTranslatePipe implements PipeTransform { + transform(key: string): string { + return 'TRANSLATED ' + key; + } +} describe('ThumbnailComponent', () => { let comp: ThumbnailComponent; @@ -14,48 +26,133 @@ describe('ThumbnailComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ThumbnailComponent, SafeUrlPipe] + declarations: [ThumbnailComponent, SafeUrlPipe, MockTranslatePipe], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(ThumbnailComponent); - comp = fixture.componentInstance; // BannerComponent test instance + comp = fixture.componentInstance; // ThumbnailComponent test instance de = fixture.debugElement.query(By.css('div.thumbnail')); el = de.nativeElement; }); - describe('when the thumbnail exists', () => { - it('should display an image', () => { - const thumbnail = new Bitstream(); + const withoutThumbnail = () => { + describe('and there is a default image', () => { + it('should display the default image', () => { + comp.src = 'http://bit.stream'; + comp.defaultImage = 'http://default.img'; + comp.errorHandler(); + expect(comp.src).toBe(comp.defaultImage); + }); + it('should include the alt text', () => { + comp.src = 'http://bit.stream'; + comp.defaultImage = 'http://default.img'; + comp.errorHandler(); + comp.ngOnChanges(); + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); + }); + }); + describe('and there is no default image', () => { + it('should display the placeholder', () => { + comp.src = 'http://default.img'; + comp.errorHandler(); + expect(comp.src).toBe(null); + + comp.ngOnChanges(); + fixture.detectChanges(); + const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement; + expect(placeholder.innerHTML).toBe('TRANSLATED ' + comp.placeholder); + }); + }); + }; + + describe('with thumbnail as Bitstream', () => { + let thumbnail: Bitstream; + beforeEach(() => { + thumbnail = new Bitstream(); thumbnail._links = { self: { href: 'self.url' }, bundle: { href: 'bundle.url' }, format: { href: 'format.url' }, content: { href: 'content.url' }, }; + }); + + it('should display an image', () => { comp.thumbnail = thumbnail; + comp.ngOnChanges(); fixture.detectChanges(); const image: HTMLElement = de.query(By.css('img')).nativeElement; expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href); }); + + it('should include the alt text', () => { + comp.thumbnail = thumbnail; + comp.ngOnChanges(); + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); + }); + + describe('when there is no thumbnail', () => { + withoutThumbnail(); + }); }); - describe(`when the thumbnail doesn't exist`, () => { - describe('and there is a default image', () => { - it('should display the default image', () => { - comp.src = 'http://bit.stream'; - comp.defaultImage = 'http://default.img'; - comp.errorHandler(); - expect(comp.src).toBe(comp.defaultImage); + + describe('with thumbnail as RemoteData', () => { + let thumbnail: RemoteData; + + describe('while loading', () => { + beforeEach(() => { + thumbnail = createPendingRemoteDataObject(); + }); + + it('should show a loading animation', () => { + comp.thumbnail = thumbnail; + comp.ngOnChanges(); + fixture.detectChanges(); + expect(de.query(By.css('ds-loading'))).toBeTruthy(); }); }); - describe('and there is no default image', () => { - it('should display the placeholder', () => { - comp.src = 'http://default.img'; - comp.defaultImage = 'http://default.img'; - comp.errorHandler(); - expect(comp.src).toBe(THUMBNAIL_PLACEHOLDER); + + describe('when there is a thumbnail', () => { + beforeEach(() => { + const bitstream = new Bitstream(); + bitstream._links = { + self: { href: 'self.url' }, + bundle: { href: 'bundle.url' }, + format: { href: 'format.url' }, + content: { href: 'content.url' }, + }; + thumbnail = createSuccessfulRemoteDataObject(bitstream); + }); + + it('should display an image', () => { + comp.thumbnail = thumbnail; + comp.ngOnChanges(); + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('src')).toBe(comp.thumbnail.payload._links.content.href); + }); + + it('should display the alt text', () => { + comp.thumbnail = thumbnail; + comp.ngOnChanges(); + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); }); }); + + describe('when there is no thumbnail', () => { + beforeEach(() => { + thumbnail = createFailedRemoteDataObject(); + }); + + withoutThumbnail(); + }); }); }); diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 7e981d5fe65..3e122cde786 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -1,61 +1,93 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnChanges } from '@angular/core'; import { Bitstream } from '../core/shared/bitstream.model'; import { hasValue } from '../shared/empty.util'; - -/** - * A fallback placeholder image as a base64 string - */ -export const THUMBNAIL_PLACEHOLDER = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'; +import { RemoteData } from '../core/data/remote-data'; /** * This component renders a given Bitstream as a thumbnail. * One input parameter of type Bitstream is expected. - * If no Bitstream is provided, a holderjs image will be rendered instead. + * If no Bitstream is provided, a HTML placeholder will be rendered instead. */ @Component({ selector: 'ds-thumbnail', styleUrls: ['./thumbnail.component.scss'], - templateUrl: './thumbnail.component.html' + templateUrl: './thumbnail.component.html', }) -export class ThumbnailComponent implements OnInit { - +export class ThumbnailComponent implements OnChanges { /** * The thumbnail Bitstream */ - @Input() thumbnail: Bitstream; + @Input() thumbnail: Bitstream | RemoteData; /** - * The default image, used if the thumbnail isn't set or can't be downloaded + * The default image, used if the thumbnail isn't set or can't be downloaded. + * If defaultImage is null, a HTML placeholder is used instead. */ - @Input() defaultImage? = THUMBNAIL_PLACEHOLDER; + @Input() defaultImage? = null; /** * The src attribute used in the template to render the image. */ - src: string; + src: string = null; + + /** + * i18n key of thumbnail alt text + */ + @Input() alt? = 'thumbnail.default.alt'; + + /** + * i18n key of HTML placeholder text + */ + @Input() placeholder? = 'thumbnail.default.placeholder'; /** - * Initialize the thumbnail. + * Limit thumbnail width to --ds-thumbnail-max-width + */ + @Input() limitWidth? = true; + + isLoading: boolean; + + /** + * Resolve the thumbnail. * Use a default image if no actual image is available. */ - ngOnInit(): void { - if (hasValue(this.thumbnail) && hasValue(this.thumbnail._links) && hasValue(this.thumbnail._links.content) && this.thumbnail._links.content.href) { - this.src = this.thumbnail._links.content.href; + ngOnChanges(): void { + if (this.thumbnail === undefined || this.thumbnail === null) { + return; + } + if (this.thumbnail instanceof Bitstream) { + this.resolveThumbnail(this.thumbnail as Bitstream); + } else { + const thumbnailRD = this.thumbnail as RemoteData; + if (thumbnailRD.isLoading) { + this.isLoading = true; + } else { + this.resolveThumbnail(thumbnailRD.payload as Bitstream); + } + } + } + + private resolveThumbnail(thumbnail: Bitstream): void { + if (hasValue(thumbnail) && hasValue(thumbnail._links) + && hasValue(thumbnail._links.content) + && thumbnail._links.content.href) { + this.src = thumbnail._links.content.href; } else { this.src = this.defaultImage; } + this.isLoading = false; } /** * Handle image download errors. * If the image can't be found, use the defaultImage instead. - * If that also can't be found, use the base64 placeholder. + * If that also can't be found, use null to fall back to the HTML placeholder. */ errorHandler() { if (this.src !== this.defaultImage) { this.src = this.defaultImage; } else { - this.src = THUMBNAIL_PLACEHOLDER; + this.src = null; } } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4c3317a0c0c..2235eda34d5 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3540,6 +3540,24 @@ + "thumbnail.default.alt": "Thumbnail Image", + + "thumbnail.default.placeholder": "No Thumbnail Available", + + "thumbnail.project.alt": "Project Logo", + + "thumbnail.project.placeholder": "Project Placeholder Image", + + "thumbnail.orgunit.alt": "OrgUnit Logo", + + "thumbnail.orgunit.placeholder": "OrgUnit Placeholder Image", + + "thumbnail.person.alt": "Profile Picture", + + "thumbnail.person.placeholder": "No Profile Picture Available", + + + "title": "DSpace", diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index 298be09f677..d0e15642815 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -46,6 +46,9 @@ --ds-edit-item-language-field-width: 43px; --ds-thumbnail-max-width: 175px; + --ds-thumbnail-placeholder-background: #{$gray-100}; + --ds-thumbnail-placeholder-border: 1px solid #{$gray-300}; + --ds-thumbnail-placeholder-color: #{lighten($gray-800, 7%)}; --ds-dso-selector-list-max-height: 475px; --ds-dso-selector-current-background-color: #eeeeee;