Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Thumbnail multiple images fallback when 404 #570

Merged
merged 7 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions libs/ui/elements/src/lib/thumbnail/thumbnail.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
<img
#imageElement
class="relative w-full object-center"
[ngClass]="objectFit === 'contain' ? 'h-4/5' : 'h-full'"
[ngStyle]="{ objectFit: objectFit }"
[ngClass]="imgFit === 'contain' ? 'h-4/5' : 'h-full'"
[ngStyle]="{ objectFit: imgFit }"
alt="thumbnail"
loading="lazy"
(load)="setObjectFit()"
[src]="imgUrl | safe: 'url'"
(error)="useFallback()"
/>
</div>
52 changes: 50 additions & 2 deletions libs/ui/elements/src/lib/thumbnail/thumbnail.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('ThumbnailComponent', () => {
describe('When no url is given', () => {
let img
beforeEach(fakeAsync(() => {
component.thumbnailUrl = undefined
component.thumbnailUrl = null
fixture.detectChanges()
img = de.query(By.css('img'))
tick(10)
Expand Down Expand Up @@ -145,7 +145,7 @@ describe('ThumbnailComponent', () => {
let img
beforeEach(() => {
component.placeholderUrl = placeholderUrl
component.thumbnailUrl = undefined
component.thumbnailUrl = null
fixture.detectChanges()
img = de.query(By.css('img'))
})
Expand Down Expand Up @@ -175,4 +175,52 @@ describe('ThumbnailComponent', () => {
expect(img.nativeElement.style.objectFit).toEqual('scale-down')
})
})

describe('thumbnail image url returns 404 and organisation logo exists', () => {
const url = 'http://test.com/img.png'
const orgLogoUrl = 'http://test.com/orgLogo.png'
const placeholderUrl = 'http://localhost/assets/img/placeholder.png'
let img
beforeEach(() => {
component.thumbnailUrl = [url, orgLogoUrl]
component.fit = ['cover', 'contain']
component.placeholderUrl = placeholderUrl
fixture.detectChanges()
img = de.query(By.css('img'))
img.nativeElement.dispatchEvent(new Event('error'))
fixture.detectChanges()
})
it('displays organisation logo', () => {
expect(img.nativeElement.src).toEqual(orgLogoUrl)
expect(img.nativeElement.style.objectFit).toEqual('contain')
})

describe('if organisation logo also returns 404', () => {
beforeEach(() => {
img.nativeElement.dispatchEvent(new Event('error'))
fixture.detectChanges()
})
it('displays placeholder', () => {
expect(img.nativeElement.src).toEqual(placeholderUrl)
expect(img.nativeElement.style.objectFit).toEqual('scale-down')
})
})
})
describe('thumbnail image url returns 404 and no organisation logo', () => {
const url = 'http://test.com/img.png'
const placeholderUrl = 'http://localhost/assets/img/placeholder.png'
let img
beforeEach(() => {
component.thumbnailUrl = url
component.placeholderUrl = placeholderUrl
fixture.detectChanges()
img = de.query(By.css('img'))
img.nativeElement.dispatchEvent(new Event('error'))
fixture.detectChanges()
})

it('displays placeholder', () => {
expect(img.nativeElement.src).toEqual(placeholderUrl)
})
})
})
82 changes: 54 additions & 28 deletions libs/ui/elements/src/lib/thumbnail/thumbnail.component.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,95 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Inject,
InjectionToken,
Input,
OnDestroy,
OnChanges,
OnInit,
Optional,
SimpleChanges,
ViewChild,
} from '@angular/core'
import { fromEvent, Subscription } from 'rxjs'

export const THUMBNAIL_PLACEHOLDER = new InjectionToken<string>(
'thumbnail-placeholder'
)

type ThumbnailImageObject = {
url: string
fit?: 'cover' | 'contain' | 'scale-down'
}

const DEFAULT_PLACEHOLDER =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH5gkNDCUFYjA1nwAAA1pJREFUeNrtnW2TmjAURh8CLlTdrmun///3tZ22+zLuYlehH7jsMGogwRiiec4MM44K6D3x3hAwAIQQQgghhJDYSM5cPwNQAMgBpACUg22GTg2gArAD8A9AKY+9CsgALAF8YRsGALwDeAWw9yGgALCKoKWP+WU8iwxjUsudLBj83sZciIiPSwhoWz7pJ5dUZFQXlEXOZ/DNeTBt3JnhBpc9aacGsOn0BuoIUk3b+5trGnEC4B7Ak4sUlInRU+wB/JbgVxG18KrTBc01EmYmcTFJQUVPy/9zTh/4BthLDOqeenB2DdBtZBN58LsSNpcUoEtTJWM/GIvMhQDde9j6h2OhXAjo6/2Q/lgkLgSQC0IBFEABZEKyiaQv5AAv7fSlS+lPVxRwOXTnEtoTPAs04yfRHGMoz8F/HOiaJfKeggLc78dmOHsVS33y9SUXsDuLlsg6FOAw/fhYhwI0pJ7WoQASpoC9p3UoQEPpaR0K0LCB3fB1e6KfAhxRweAKgQ5PsQxJ+CzCJYC/A7+EWt4TzVCE77GgEsBPHA/G7QBswcE4b+noVRZ2QxkCCqAAQgEUQCiAAggFUAChAAogFEABhAJcfT9FAdOxQuB/ML9lAfdo/qWYy2MK8EiO5mrrliUCvdIudAEzWWzQzWuxwjRnAK9WwAzAWhZTCe3l7cryNQrQBL/txZhKGGrlwc36ogIPfvdzfhuQsDDM80EVZXUFwe+mEJ2EOwBfLfYTTFFWVxL8Qwl3nedSye1jjhEyCjAPflfCWiScU1iDKMrqyoJ/KOFxRDf1sCg/xCxgTPC7EnIHn6GYsiirKw2+ayYryorBn7YoKwb/qCgntywg1OBPdqSsGPyTRXnp07gvPgD8AAl6KIICCAVQAAlLwOg5MSNi9NyqJgKqAHpQoZNZxs5KgG7SDN7AZzgWOxcCtprn5/wVfLb+uWXsrASUPXlvHbmETGKQjBVgMitVJTuaaQS2t/GoEMeE3onEor2jlK4RvwN4G1u9T4n6zp6PMTWAXzCYdCq12OCOhdeYZzT3mIErAW1Fr+HmNOAt82KSesYIAJoRzR2aIVumo+Ms8WwT/HOOZlM0Y+Zzxv2zp/gCTzfzPOyGtdfgp7LEcDvbvSxbWXg/HUIIIYQQQogx/wHLoX7NoCMFPwAAAABJRU5ErkJggg=='

type FitOptions = 'cover' | 'contain' | 'scale-down'

@Component({
selector: 'gn-ui-thumbnail',
templateUrl: './thumbnail.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ThumbnailComponent implements AfterViewInit, OnDestroy {
@Input() set thumbnailUrl(url: string) {
this.imgUrl = url || this.placeholderUrl
this.isPlaceholder = !url
}
@Input() fit: 'cover' | 'contain' | 'scale-down' = 'cover'
export class ThumbnailComponent implements OnInit, OnChanges {
@Input() thumbnailUrl: string | string[]
@Input() fit: FitOptions | FitOptions[] = 'cover'
@ViewChild('imageElement') imgElement: ElementRef<HTMLImageElement>
@ViewChild('containerElement') containerElement: ElementRef<HTMLDivElement>
imgUrl: string
imgFit: FitOptions
placeholderUrl = this.optionalPlaceholderUrl || DEFAULT_PLACEHOLDER
isPlaceholder = false
sub: Subscription

get objectFit() {
return this.isPlaceholder ? 'scale-down' : this.fit
}
private images: ThumbnailImageObject[] = []

constructor(
@Optional()
@Inject(THUMBNAIL_PLACEHOLDER)
private optionalPlaceholderUrl: string,
private changeDetector: ChangeDetectorRef
private optionalPlaceholderUrl: string
) {}

ngAfterViewInit() {
this.sub = fromEvent(this.imgElement.nativeElement, 'error').subscribe(() =>
this.useFallback()
)
ngOnInit() {
this.updateImageList()
}

ngOnChanges(changes: SimpleChanges): void {
if (!('thumbnailUrl' in changes) && !('fit' in changes)) {
return
}
this.updateImageList()
}

private updateImageList() {
if (!this.thumbnailUrl) {
this.setPlaceholder()
return
}
const urls = Array.isArray(this.thumbnailUrl)
? this.thumbnailUrl
: [this.thumbnailUrl]
this.images = urls.map((url, index) => ({
url,
fit: (Array.isArray(this.fit) ? this.fit[index] : this.fit) || 'cover',
}))
this.setNewSrcImage(this.images[0])
}

private setNewSrcImage(image: ThumbnailImageObject) {
this.imgFit = image.fit
this.imgUrl = image.url
}

ngOnDestroy() {
this.sub.unsubscribe()
private setPlaceholder(): void {
this.isPlaceholder = true
this.setNewSrcImage({ url: this.placeholderUrl, fit: 'scale-down' })
}

useFallback() {
if (!this.isPlaceholder) {
this.isPlaceholder = true
this.imgUrl = this.placeholderUrl
this.changeDetector.detectChanges()
if (this.images.length > 1) {
this.images.shift()
this.setNewSrcImage(this.images[0])
} else {
this.setPlaceholder()
}
}

Expand All @@ -74,7 +100,7 @@ export class ThumbnailComponent implements AfterViewInit, OnDestroy {
this.imgElement.nativeElement.naturalWidth < cw &&
this.imgElement.nativeElement.naturalHeight < ch
) {
this.fit = 'scale-down'
this.imgFit = 'scale-down'
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
>
<gn-ui-thumbnail
class="relative h-full w-full object-cover object-left-top"
[thumbnailUrl]="record.thumbnailUrl || contact.logoUrl"
[fit]="record.thumbnailUrl ? 'cover' : 'contain'"
[thumbnailUrl]="[record.thumbnailUrl, contact.logoUrl]"
[fit]="['cover', 'contain']"
></gn-ui-thumbnail>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
Component,
Input,
Output,
ElementRef,
EventEmitter,
OnInit,
Input,
OnDestroy,
ElementRef,
OnInit,
Output,
TemplateRef,
} from '@angular/core'
import {
Expand Down
3 changes: 0 additions & 3 deletions libs/util/app-config/src/lib/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@

export function getCustomTranslations(langCode: string): CustomTranslations {
if (customTranslations === null) throw new Error(MISSING_CONFIG_ERROR)
console.log(
langCode in customTranslations ? customTranslations[langCode] : {}
)
return langCode in customTranslations ? customTranslations[langCode] : {}
}

Expand All @@ -67,8 +64,8 @@
.then((conf) => {
let parsed
try {
parsed = TOML.parse(conf, { joiner: '\n', bigint: false }) as any

Check warning on line 67 in libs/util/app-config/src/lib/app-config.ts

View workflow job for this annotation

GitHub Actions / Format check, lint, unit tests

Unexpected any. Specify a different type
} catch (e: any) {

Check warning on line 68 in libs/util/app-config/src/lib/app-config.ts

View workflow job for this annotation

GitHub Actions / Format check, lint, unit tests

Unexpected any. Specify a different type
throw new Error(
`An error occurred when parsing the configuration file: ${e.message}`
)
Expand Down
Loading