Merge pull request #2235 from alexandrevryghem/themed-thumbnail_component-main

Fixed `ngOnChanges` not working for themed components & decorator components + minor fixes
This commit is contained in:
Tim Donohue
2023-05-19 11:57:13 -05:00
committed by GitHub
56 changed files with 385 additions and 194 deletions

View File

@@ -2,7 +2,7 @@
<div class="container" *ngVar="(bitstreamFormatsRD$ | async) as formatsRD"> <div class="container" *ngVar="(bitstreamFormatsRD$ | async) as formatsRD">
<div class="row" *ngIf="bitstreamRD?.hasSucceeded && formatsRD?.hasSucceeded"> <div class="row" *ngIf="bitstreamRD?.hasSucceeded && formatsRD?.hasSucceeded">
<div class="col-md-2"> <div class="col-md-2">
<ds-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-themed-thumbnail>
</div> </div>
<div class="col-md-10"> <div class="col-md-10">
<div class="container"> <div class="container">

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -7,7 +7,7 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="object?.thumbnail | async"></ds-themed-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"
[fields]="['publicationvolume.volumeNumber']" [fields]="['publicationvolume.volumeNumber']"

View File

@@ -7,7 +7,7 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="object?.thumbnail | async"></ds-themed-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"
[fields]="['publicationvolume.volumeNumber']" [fields]="['publicationvolume.volumeNumber']"

View File

@@ -7,7 +7,7 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="object?.thumbnail | async"></ds-themed-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-generic-item-page-field class="item-page-fields" [item]="object" <ds-generic-item-page-field class="item-page-fields" [item]="object"
[fields]="['creativeworkseries.issn']" [fields]="['creativeworkseries.issn']"

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -7,12 +7,12 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="object?.thumbnail | async" <ds-themed-thumbnail [thumbnail]="object?.thumbnail | async"
[defaultImage]="'assets/images/orgunit-placeholder.svg'" [defaultImage]="'assets/images/orgunit-placeholder.svg'"
[alt]="'thumbnail.orgunit.alt'" [alt]="'thumbnail.orgunit.alt'"
[placeholder]="'thumbnail.orgunit.placeholder'" [placeholder]="'thumbnail.orgunit.placeholder'"
> >
</ds-thumbnail> </ds-themed-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"
[fields]="['organization.foundingDate']" [fields]="['organization.foundingDate']"

View File

@@ -7,11 +7,11 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="object?.thumbnail | async" <ds-themed-thumbnail [thumbnail]="object?.thumbnail | async"
[defaultImage]="'assets/images/person-placeholder.svg'" [defaultImage]="'assets/images/person-placeholder.svg'"
[alt]="'thumbnail.person.alt'" [alt]="'thumbnail.person.alt'"
[placeholder]="'thumbnail.person.placeholder'"> [placeholder]="'thumbnail.person.placeholder'">
</ds-thumbnail> </ds-themed-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"
[fields]="['person.email']" [fields]="['person.email']"

View File

@@ -7,12 +7,12 @@
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail <ds-themed-thumbnail
[thumbnail]="object?.thumbnail | async" [thumbnail]="object?.thumbnail | async"
[defaultImage]="'assets/images/project-placeholder.svg'" [defaultImage]="'assets/images/project-placeholder.svg'"
[alt]="'thumbnail.project.alt'" [alt]="'thumbnail.project.alt'"
[placeholder]="'thumbnail.project.placeholder'"> [placeholder]="'thumbnail.project.placeholder'">
</ds-thumbnail> </ds-themed-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<!--<ds-generic-item-page-field [item]="object"--> <!--<ds-generic-item-page-field [item]="object"-->
<!--[fields]="['project.identifier.status']"--> <!--[fields]="['project.identifier.status']"-->

View File

@@ -1,12 +1,12 @@
<ng-template #descTemplate> <ng-template #descTemplate>
<span class="text-muted"> <span class="text-muted">
<span class="item-list-job-title"> <span class="item-list-job-title">
<span [innerHTML]="metadataRepresentation.firstMetadataValue(['dc.description'])"></span> <span [innerHTML]="mdRepresentation.firstMetadataValue(['dc.description'])"></span>
</span> </span>
</span> </span>
</ng-template> </ng-template>
<ds-truncatable [id]="metadataRepresentation.id"> <ds-truncatable [id]="mdRepresentation.id">
<a [routerLink]="[itemPageRoute]" <a [routerLink]="[itemPageRoute]"
[innerHTML]="metadataRepresentation.getValue()" [innerHTML]="mdRepresentation.getValue()"
[ngbTooltip]="metadataRepresentation.allMetadata(['dc.description']).length > 0 ? descTemplate : null"></a> [ngbTooltip]="mdRepresentation.allMetadata(['dc.description']).length > 0 ? descTemplate : null"></a>
</ds-truncatable> </ds-truncatable>

View File

@@ -34,7 +34,7 @@ describe('OrgUnitItemMetadataListElementComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(OrgUnitItemMetadataListElementComponent); fixture = TestBed.createComponent(OrgUnitItemMetadataListElementComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.metadataRepresentation = mockItemMetadataRepresentation; comp.mdRepresentation = mockItemMetadataRepresentation;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,15 +1,15 @@
<ng-template #descTemplate> <ng-template #descTemplate>
<span class="text-muted"> <span class="text-muted">
<span *ngIf="metadataRepresentation.allMetadata(['person.jobTitle']).length > 0" <span *ngIf="mdRepresentation.allMetadata(['person.jobTitle']).length > 0"
class="item-list-job-title"> class="item-list-job-title">
<span *ngFor="let value of metadataRepresentation.allMetadataValues(['person.jobTitle']); let last=last;"> <span *ngFor="let value of mdRepresentation.allMetadataValues(['person.jobTitle']); let last=last;">
<span [innerHTML]="value"><span [innerHTML]="value"></span></span> <span [innerHTML]="value"><span [innerHTML]="value"></span></span>
</span> </span>
</span> </span>
</span> </span>
</ng-template> </ng-template>
<ds-truncatable [id]="metadataRepresentation.id"> <ds-truncatable [id]="mdRepresentation.id">
<a [routerLink]="[itemPageRoute]" <a [routerLink]="[itemPageRoute]"
[innerHTML]="metadataRepresentation.getValue()" [innerHTML]="mdRepresentation.getValue()"
[ngbTooltip]="metadataRepresentation.allMetadata(['person.jobTitle']).length > 0 ? descTemplate : null"></a> [ngbTooltip]="mdRepresentation.allMetadata(['person.jobTitle']).length > 0 ? descTemplate : null"></a>
</ds-truncatable> </ds-truncatable>

View File

@@ -36,7 +36,7 @@ describe('PersonItemMetadataListElementComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(PersonItemMetadataListElementComponent); fixture = TestBed.createComponent(PersonItemMetadataListElementComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.metadataRepresentation = mockItemMetadataRepresentation; comp.mdRepresentation = mockItemMetadataRepresentation;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,6 +1,6 @@
<div class="d-flex"> <div class="d-flex">
<!-- <div class="person-thumbnail pr-2">--> <!-- <div class="person-thumbnail pr-2">-->
<!-- <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail>--> <!-- <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-themed-thumbnail>-->
<!-- </div>--> <!-- </div>-->
<div class="flex-grow-1"> <div class="flex-grow-1">
<ds-org-unit-input-suggestions *ngIf="useNameVariants" [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)" <ds-org-unit-input-suggestions *ngIf="useNameVariants" [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)"

View File

@@ -13,7 +13,7 @@
<div class="file-section row mb-3" *ngFor="let file of originals?.page;"> <div class="file-section row mb-3" *ngFor="let file of originals?.page;">
<div class="col-3"> <div class="col-3">
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-themed-thumbnail>
</div> </div>
<div class="col-7"> <div class="col-7">
<dl class="row"> <dl class="row">
@@ -55,7 +55,7 @@
<div class="file-section row" *ngFor="let file of licenses?.page;"> <div class="file-section row" *ngFor="let file of licenses?.page;">
<div class="col-3"> <div class="col-3">
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-themed-thumbnail>
</div> </div>
<div class="col-7"> <div class="col-7">
<dl class="row"> <dl class="row">

View File

@@ -17,7 +17,7 @@
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ng-container *ngIf="!(mediaViewer.image || mediaViewer.video)"> <ng-container *ngIf="!(mediaViewer.image || mediaViewer.video)">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="object?.thumbnail | async"></ds-themed-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
</ng-container> </ng-container>
<div *ngIf="mediaViewer.image || mediaViewer.video" class="mb-2"> <div *ngIf="mediaViewer.image || mediaViewer.video" class="mb-2">

View File

@@ -18,7 +18,7 @@
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ng-container *ngIf="!(mediaViewer.image || mediaViewer.video)"> <ng-container *ngIf="!(mediaViewer.image || mediaViewer.video)">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="object?.thumbnail | async"></ds-themed-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
</ng-container> </ng-container>
<div *ngIf="mediaViewer.image || mediaViewer.video" class="mb-2"> <div *ngIf="mediaViewer.image || mediaViewer.video" class="mb-2">

View File

@@ -1,4 +1,4 @@
import { Component, ComponentFactoryResolver, Inject, Input, OnInit, ViewChild } from '@angular/core'; import { Component, ComponentFactoryResolver, Inject, Input, OnInit, ViewChild, OnChanges, SimpleChanges, ComponentRef, ViewContainerRef, ComponentFactory } from '@angular/core';
import { import {
MetadataRepresentation, MetadataRepresentation,
MetadataRepresentationType MetadataRepresentationType
@@ -8,19 +8,17 @@ import { Context } from '../../core/shared/context.model';
import { GenericConstructor } from '../../core/shared/generic-constructor'; import { GenericConstructor } from '../../core/shared/generic-constructor';
import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component'; import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component';
import { MetadataRepresentationDirective } from './metadata-representation.directive'; import { MetadataRepresentationDirective } from './metadata-representation.directive';
import { hasValue } from '../empty.util'; import { hasValue, isNotEmpty, hasNoValue } from '../empty.util';
import { ThemeService } from '../theme-support/theme.service'; import { ThemeService } from '../theme-support/theme.service';
@Component({ @Component({
selector: 'ds-metadata-representation-loader', selector: 'ds-metadata-representation-loader',
// styleUrls: ['./metadata-representation-loader.component.scss'],
templateUrl: './metadata-representation-loader.component.html' templateUrl: './metadata-representation-loader.component.html'
}) })
/** /**
* Component for determining what component to use depending on the item's entity type (dspace.entity.type), its metadata representation and, optionally, its context * Component for determining what component to use depending on the item's entity type (dspace.entity.type), its metadata representation and, optionally, its context
*/ */
export class MetadataRepresentationLoaderComponent implements OnInit { export class MetadataRepresentationLoaderComponent implements OnInit, OnChanges {
private componentRefInstance: MetadataRepresentationListElementComponent;
/** /**
* The item or metadata to determine the component for * The item or metadata to determine the component for
@@ -31,8 +29,8 @@ export class MetadataRepresentationLoaderComponent implements OnInit {
} }
@Input() set mdRepresentation(nextValue: MetadataRepresentation) { @Input() set mdRepresentation(nextValue: MetadataRepresentation) {
this._mdRepresentation = nextValue; this._mdRepresentation = nextValue;
if (hasValue(this.componentRefInstance)) { if (hasValue(this.compRef?.instance)) {
this.componentRefInstance.metadataRepresentation = nextValue; this.compRef.instance.mdRepresentation = nextValue;
} }
} }
@@ -46,6 +44,16 @@ export class MetadataRepresentationLoaderComponent implements OnInit {
*/ */
@ViewChild(MetadataRepresentationDirective, {static: true}) mdRepDirective: MetadataRepresentationDirective; @ViewChild(MetadataRepresentationDirective, {static: true}) mdRepDirective: MetadataRepresentationDirective;
/**
* The reference to the dynamic component
*/
protected compRef: ComponentRef<MetadataRepresentationListElementComponent>;
protected inAndOutputNames: (keyof this)[] = [
'context',
'mdRepresentation',
];
constructor( constructor(
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private themeService: ThemeService, private themeService: ThemeService,
@@ -57,14 +65,41 @@ export class MetadataRepresentationLoaderComponent implements OnInit {
* Set up the dynamic child component * Set up the dynamic child component
*/ */
ngOnInit(): void { ngOnInit(): void {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent()); this.instantiateComponent();
}
const viewContainerRef = this.mdRepDirective.viewContainerRef; /**
* Whenever the inputs change, update the inputs of the dynamic component
*/
ngOnChanges(changes: SimpleChanges): void {
if (hasNoValue(this.compRef)) {
// sometimes the component has not been initialized yet, so it first needs to be initialized
// before being called again
this.instantiateComponent(changes);
} else {
// if an input or output has changed
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
this.connectInputsAndOutputs();
if (this.compRef?.instance && 'ngOnChanges' in this.compRef.instance) {
(this.compRef.instance as any).ngOnChanges(changes);
}
}
}
}
private instantiateComponent(changes?: SimpleChanges): void {
const componentFactory: ComponentFactory<MetadataRepresentationListElementComponent> = this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
const viewContainerRef: ViewContainerRef = this.mdRepDirective.viewContainerRef;
viewContainerRef.clear(); viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory); this.compRef = viewContainerRef.createComponent(componentFactory);
this.componentRefInstance = componentRef.instance as MetadataRepresentationListElementComponent;
this.componentRefInstance.metadataRepresentation = this.mdRepresentation; if (hasValue(changes)) {
this.ngOnChanges(changes);
} else {
this.connectInputsAndOutputs();
}
} }
/** /**
@@ -74,4 +109,16 @@ export class MetadataRepresentationLoaderComponent implements OnInit {
private getComponent(): GenericConstructor<MetadataRepresentationListElementComponent> { private getComponent(): GenericConstructor<MetadataRepresentationListElementComponent> {
return this.getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context, this.themeService.getThemeName()); return this.getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context, this.themeService.getThemeName());
} }
/**
* Connect the in and outputs of this component to the dynamic component,
* to ensure they're in sync
*/
protected connectInputsAndOutputs(): void {
if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
this.inAndOutputNames.filter((name: any) => this[name] !== undefined).forEach((name: any) => {
this.compRef.instance[name] = this[name];
});
}
}
} }

View File

@@ -3,20 +3,21 @@ import {
ComponentFactoryResolver, ComponentFactoryResolver,
EventEmitter, EventEmitter,
Input, Input,
OnDestroy,
OnInit, OnInit,
Output, Output,
ViewChild ViewChild,
OnChanges,
SimpleChanges,
ComponentRef,
} from '@angular/core'; } from '@angular/core';
import { getComponentByWorkflowTaskOption } from './claimed-task-actions-decorator'; import { getComponentByWorkflowTaskOption } from './claimed-task-actions-decorator';
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive'; import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component'; import { hasValue, isNotEmpty, hasNoValue } from '../../../empty.util';
import { hasValue } from '../../../empty.util';
import { Subscription } from 'rxjs';
import { MyDSpaceActionsResult } from '../../mydspace-actions'; import { MyDSpaceActionsResult } from '../../mydspace-actions';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
@Component({ @Component({
selector: 'ds-claimed-task-actions-loader', selector: 'ds-claimed-task-actions-loader',
@@ -26,7 +27,7 @@ import { WorkflowItem } from '../../../../core/submission/models/workflowitem.mo
* Component for loading a ClaimedTaskAction component depending on the "option" input * Component for loading a ClaimedTaskAction component depending on the "option" input
* Passes on the ClaimedTask to the component and subscribes to the processCompleted output * Passes on the ClaimedTask to the component and subscribes to the processCompleted output
*/ */
export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy { export class ClaimedTaskActionsLoaderComponent implements OnInit, OnChanges {
/** /**
* The item object that belonging to the ClaimedTask object * The item object that belonging to the ClaimedTask object
*/ */
@@ -59,10 +60,18 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
@ViewChild(ClaimedTaskActionsDirective, {static: true}) claimedTaskActionsDirective: ClaimedTaskActionsDirective; @ViewChild(ClaimedTaskActionsDirective, {static: true}) claimedTaskActionsDirective: ClaimedTaskActionsDirective;
/** /**
* Array to track all subscriptions and unsubscribe them onDestroy * The reference to the dynamic component
* @type {Array}
*/ */
protected subs: Subscription[] = []; protected compRef: ComponentRef<Component>;
/**
* The list of input and output names for the dynamic component
*/
protected inAndOutputNames: (keyof ClaimedTaskActionsAbstractComponent & keyof this)[] = [
'object',
'option',
'processCompleted',
];
constructor(private componentFactoryResolver: ComponentFactoryResolver) { constructor(private componentFactoryResolver: ComponentFactoryResolver) {
} }
@@ -71,7 +80,29 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
* Fetch, create and initialize the relevant component * Fetch, create and initialize the relevant component
*/ */
ngOnInit(): void { ngOnInit(): void {
this.instantiateComponent();
}
/**
* Whenever the inputs change, update the inputs of the dynamic component
*/
ngOnChanges(changes: SimpleChanges): void {
if (hasNoValue(this.compRef)) {
// sometimes the component has not been initialized yet, so it first needs to be initialized
// before being called again
this.instantiateComponent(changes);
} else {
// if an input or output has changed
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
this.connectInputsAndOutputs();
if (this.compRef?.instance && 'ngOnChanges' in this.compRef.instance) {
(this.compRef.instance as any).ngOnChanges(changes);
}
}
}
}
private instantiateComponent(changes?: SimpleChanges): void {
const comp = this.getComponentByWorkflowTaskOption(this.option); const comp = this.getComponentByWorkflowTaskOption(this.option);
if (hasValue(comp)) { if (hasValue(comp)) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp); const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp);
@@ -79,13 +110,12 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
const viewContainerRef = this.claimedTaskActionsDirective.viewContainerRef; const viewContainerRef = this.claimedTaskActionsDirective.viewContainerRef;
viewContainerRef.clear(); viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory); this.compRef = viewContainerRef.createComponent(componentFactory);
const componentInstance = (componentRef.instance as ClaimedTaskActionsAbstractComponent);
componentInstance.item = this.item; if (hasValue(changes)) {
componentInstance.object = this.object; this.ngOnChanges(changes);
componentInstance.workflowitem = this.workflowitem; } else {
if (hasValue(componentInstance.processCompleted)) { this.connectInputsAndOutputs();
this.subs.push(componentInstance.processCompleted.subscribe((result) => this.processCompleted.emit(result)));
} }
} }
} }
@@ -95,11 +125,14 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
} }
/** /**
* Unsubscribe from open subscriptions * Connect the in and outputs of this component to the dynamic component,
* to ensure they're in sync
*/ */
ngOnDestroy(): void { protected connectInputsAndOutputs(): void {
this.subs if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
.filter((subscription) => hasValue(subscription)) this.inAndOutputNames.filter((name: any) => this[name] !== undefined).forEach((name: any) => {
.forEach((subscription) => subscription.unsubscribe()); this.compRef.instance[name] = this[name];
});
}
} }
} }

View File

@@ -91,7 +91,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
(listableComponent as any).reloadedObject.emit(reloadedObject); (listableComponent as any).reloadedObject.emit(reloadedObject);
tick(200); tick(200);
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject); expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject, undefined);
})); }));
it('should re-emit it as a contentChange', fakeAsync(() => { it('should re-emit it as a contentChange', fakeAsync(() => {

View File

@@ -12,7 +12,7 @@ import {
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription, combineLatest, of as observableOf, Observable } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { ListableObject } from '../listable-object.model'; import { ListableObject } from '../listable-object.model';
@@ -22,7 +22,7 @@ import { getListableObjectComponent } from './listable-object.decorator';
import { GenericConstructor } from '../../../../core/shared/generic-constructor'; import { GenericConstructor } from '../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from './listable-object.directive'; import { ListableObjectDirective } from './listable-object.directive';
import { CollectionElementLinkType } from '../../collection-element-link.type'; import { CollectionElementLinkType } from '../../collection-element-link.type';
import { hasValue, isNotEmpty } from '../../../empty.util'; import { hasValue, isNotEmpty, hasNoValue } from '../../../empty.util';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { ThemeService } from '../../../theme-support/theme.service'; import { ThemeService } from '../../../theme-support/theme.service';
@@ -126,8 +126,18 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
* Whenever the inputs change, update the inputs of the dynamic component * Whenever the inputs change, update the inputs of the dynamic component
*/ */
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) { if (hasNoValue(this.compRef)) {
this.connectInputsAndOutputs(); // sometimes the component has not been initialized yet, so it first needs to be initialized
// before being called again
this.instantiateComponent(this.object, changes);
} else {
// if an input or output has changed
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
this.connectInputsAndOutputs();
if (this.compRef?.instance && 'ngOnChanges' in this.compRef.instance) {
(this.compRef.instance as any).ngOnChanges(changes);
}
}
} }
} }
@@ -137,7 +147,7 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
.forEach((subscription) => subscription.unsubscribe()); .forEach((subscription) => subscription.unsubscribe());
} }
private instantiateComponent(object) { private instantiateComponent(object: ListableObject, changes?: SimpleChanges): void {
const component = this.getComponent(object.getRenderTypes(), this.viewMode, this.context); const component = this.getComponent(object.getRenderTypes(), this.viewMode, this.context);
@@ -151,16 +161,21 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
} }
); );
this.connectInputsAndOutputs(); if (hasValue(changes)) {
this.ngOnChanges(changes);
} else {
this.connectInputsAndOutputs();
}
if ((this.compRef.instance as any).reloadedObject) { if ((this.compRef.instance as any).reloadedObject) {
(this.compRef.instance as any).reloadedObject.pipe( combineLatest([
take(1) observableOf(changes),
).subscribe((reloadedObject: DSpaceObject) => { (this.compRef.instance as any).reloadedObject.pipe(take(1)) as Observable<DSpaceObject>,
]).subscribe(([simpleChanges, reloadedObject]: [SimpleChanges, DSpaceObject]) => {
if (reloadedObject) { if (reloadedObject) {
this.compRef.destroy(); this.compRef.destroy();
this.object = reloadedObject; this.object = reloadedObject;
this.instantiateComponent(reloadedObject); this.instantiateComponent(reloadedObject, simpleChanges);
this.cdr.detectChanges(); this.cdr.detectChanges();
this.contentChange.emit(reloadedObject); this.contentChange.emit(reloadedObject);
} }
@@ -184,7 +199,7 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
*/ */
protected connectInputsAndOutputs(): void { protected connectInputsAndOutputs(): void {
if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) { if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
this.inAndOutputNames.forEach((name: any) => { this.inAndOutputNames.filter((name: any) => this[name] !== undefined).forEach((name: any) => {
this.compRef.instance[name] = this[name]; this.compRef.instance[name] = this[name];
}); });
} }

View File

@@ -14,7 +14,7 @@ import { followLink } from '../../../utils/follow-link-config.model';
import { LinkService } from '../../../../core/cache/builders/link.service'; import { LinkService } from '../../../../core/cache/builders/link.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { isNotEmpty } from '../../../empty.util'; import { isNotEmpty, hasValue } from '../../../empty.util';
import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
import { Context } from 'src/app/core/shared/context.model'; import { Context } from 'src/app/core/shared/context.model';
@@ -86,7 +86,9 @@ export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultD
ngOnDestroy() { ngOnDestroy() {
// This ensures the object is removed from cache, when action is performed on task // This ensures the object is removed from cache, when action is performed on task
this.objectCache.remove(this.dso._links.workflowitem.href); if (hasValue(this.dso)) {
this.objectCache.remove(this.dso._links.workflowitem.href);
}
} }
} }

View File

@@ -6,7 +6,7 @@
<div class="row mb-1"> <div class="row mb-1">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="item?.thumbnail | async"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="item?.thumbnail | async"></ds-themed-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ng-container *ngVar="(getFiles() | async) as bitstreams"> <ng-container *ngVar="(getFiles() | async) as bitstreams">
<ds-metadata-field-wrapper [label]="('item.page.files' | translate)"> <ds-metadata-field-wrapper [label]="('item.page.files' | translate)">

View File

@@ -14,7 +14,7 @@ import { followLink } from '../../../utils/follow-link-config.model';
import { LinkService } from '../../../../core/cache/builders/link.service'; import { LinkService } from '../../../../core/cache/builders/link.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { isNotEmpty } from '../../../empty.util'; import { isNotEmpty, hasValue } from '../../../empty.util';
import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
import { Context } from 'src/app/core/shared/context.model'; import { Context } from 'src/app/core/shared/context.model';
@@ -87,7 +87,9 @@ export class PoolSearchResultDetailElementComponent extends SearchResultDetailEl
ngOnDestroy() { ngOnDestroy() {
// This ensures the object is removed from cache, when action is performed on task // This ensures the object is removed from cache, when action is performed on task
this.objectCache.remove(this.dso._links.workflowitem.href); if (hasValue(this.dso)) {
this.objectCache.remove(this.dso._links.workflowitem.href);
}
} }
} }

View File

@@ -1,11 +1,11 @@
<div class="card"> <div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', object.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', object.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top"> <span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</span> </span>
<div class="card-body"> <div class="card-body">
<h4 class="card-title">{{object.name}}</h4> <h4 class="card-title">{{object.name}}</h4>

View File

@@ -1,11 +1,11 @@
<div class="card"> <div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', object.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', object.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top"> <span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</span> </span>
<div class="card-body"> <div class="card-body">
<h4 class="card-title">{{object.name}}</h4> <h4 class="card-title">{{object.name}}</h4>

View File

@@ -1,11 +1,11 @@
<div class="card"> <div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top"> <span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</span> </span>
<div class="card-body"> <div class="card-body">
<ds-themed-badges *ngIf="showLabel" [object]="dso" [context]="context"></ds-themed-badges> <ds-themed-badges *ngIf="showLabel" [object]="dso" [context]="context"></ds-themed-badges>

View File

@@ -1,11 +1,11 @@
<div class="card"> <div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top"> <span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</span> </span>
<div class="card-body"> <div class="card-body">
<ds-themed-badges *ngIf="showLabel" [object]="dso" [context]="context"></ds-themed-badges> <ds-themed-badges *ngIf="showLabel" [object]="dso" [context]="context"></ds-themed-badges>

View File

@@ -5,14 +5,14 @@
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[itemPageRoute]" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</a> </a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-themed-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-themed-thumbnail>
</div> </div>
</span> </span>
<div class="card-body"> <div class="card-body">

View File

@@ -1,9 +1,9 @@
<div> <div>
<a *ngIf="(metadataRepresentation.representationType=='browse_link')" <a *ngIf="(mdRepresentation.representationType=='browse_link')"
target="_blank" class="dont-break-out" target="_blank" class="dont-break-out"
[routerLink]="['/browse/', metadataRepresentation.browseDefinition.id]" [routerLink]="['/browse/', mdRepresentation.browseDefinition.id]"
[queryParams]="getQueryParams()"> [queryParams]="getQueryParams()">
{{metadataRepresentation.getValue()}} {{mdRepresentation.getValue()}}
</a> </a>
<b>(new browse link page)</b> <b>(new browse link page)</b>
</div> </div>

View File

@@ -30,7 +30,7 @@ describe('BrowseLinkMetadataListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent); fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.metadataRepresentation = mockMetadataRepresentation; comp.mdRepresentation = mockMetadataRepresentation;
fixture.detectChanges(); fixture.detectChanges();
})); }));
@@ -46,7 +46,7 @@ describe('BrowseLinkMetadataListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent); fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.metadataRepresentation = mockMetadataRepresentationWithUrl; comp.mdRepresentation = mockMetadataRepresentationWithUrl;
fixture.detectChanges(); fixture.detectChanges();
})); }));

View File

@@ -20,9 +20,9 @@ export class BrowseLinkMetadataListElementComponent extends MetadataRepresentati
* expects 'startsWith' (eg browse by date) or 'value' (eg browse by title) * expects 'startsWith' (eg browse by date) or 'value' (eg browse by title)
*/ */
getQueryParams() { getQueryParams() {
let queryParams = {startsWith: this.metadataRepresentation.getValue()}; let queryParams = {startsWith: this.mdRepresentation.getValue()};
if (this.metadataRepresentation.browseDefinition.metadataBrowse) { if (this.mdRepresentation.browseDefinition.metadataBrowse) {
return {value: this.metadataRepresentation.getValue()}; return {value: this.mdRepresentation.getValue()};
} }
return queryParams; return queryParams;
} }

View File

@@ -1 +1 @@
<ds-listable-object-component-loader [object]="metadataRepresentation" [viewMode]="viewMode"></ds-listable-object-component-loader> <ds-listable-object-component-loader [object]="mdRepresentation" [viewMode]="viewMode"></ds-listable-object-component-loader>

View File

@@ -23,7 +23,7 @@ describe('ItemMetadataListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(ItemMetadataListElementComponent); fixture = TestBed.createComponent(ItemMetadataListElementComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.metadataRepresentation = mockItemMetadataRepresentation; comp.mdRepresentation = mockItemMetadataRepresentation;
fixture.detectChanges(); fixture.detectChanges();
})); }));

View File

@@ -1,5 +1,5 @@
import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, Input } from '@angular/core';
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths'; import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths';
@@ -11,7 +11,7 @@ import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths'
* An abstract class for displaying a single ItemMetadataRepresentation * An abstract class for displaying a single ItemMetadataRepresentation
*/ */
export class ItemMetadataRepresentationListElementComponent extends MetadataRepresentationListElementComponent implements OnInit { export class ItemMetadataRepresentationListElementComponent extends MetadataRepresentationListElementComponent implements OnInit {
metadataRepresentation: ItemMetadataRepresentation; @Input() mdRepresentation: ItemMetadataRepresentation;
/** /**
* Route to the item's page * Route to the item's page
@@ -19,6 +19,6 @@ export class ItemMetadataRepresentationListElementComponent extends MetadataRepr
itemPageRoute: string; itemPageRoute: string;
ngOnInit(): void { ngOnInit(): void {
this.itemPageRoute = getItemPageRoute(this.metadataRepresentation); this.itemPageRoute = getItemPageRoute(this.mdRepresentation);
} }
} }

View File

@@ -36,7 +36,7 @@ describe('MetadataRepresentationListElementComponent', () => {
describe('when the value is not a URL', () => { describe('when the value is not a URL', () => {
beforeEach(() => { beforeEach(() => {
comp.metadataRepresentation = mockMetadataRepresentation; comp.mdRepresentation = mockMetadataRepresentation;
}); });
it('isLink correctly detects a non-URL string as false', () => { it('isLink correctly detects a non-URL string as false', () => {
waitForAsync(() => { waitForAsync(() => {
@@ -47,7 +47,7 @@ describe('MetadataRepresentationListElementComponent', () => {
describe('when the value is a URL', () => { describe('when the value is a URL', () => {
beforeEach(() => { beforeEach(() => {
comp.metadataRepresentation = mockMetadataRepresentationUrl; comp.mdRepresentation = mockMetadataRepresentationUrl;
}); });
it('isLink correctly detects a URL string as true', () => { it('isLink correctly detects a URL string as true', () => {
waitForAsync(() => { waitForAsync(() => {

View File

@@ -1,5 +1,6 @@
import { Component } from '@angular/core'; import { Component, Input } from '@angular/core';
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
import { Context } from '../../../core/shared/context.model';
@Component({ @Component({
selector: 'ds-metadata-representation-list-element', selector: 'ds-metadata-representation-list-element',
@@ -9,10 +10,15 @@ import { MetadataRepresentation } from '../../../core/shared/metadata-representa
* An abstract class for displaying a single MetadataRepresentation * An abstract class for displaying a single MetadataRepresentation
*/ */
export class MetadataRepresentationListElementComponent { export class MetadataRepresentationListElementComponent {
/**
* The optional context
*/
@Input() context: Context;
/** /**
* The metadata representation of this component * The metadata representation of this component
*/ */
metadataRepresentation: MetadataRepresentation; @Input() mdRepresentation: MetadataRepresentation;
/** /**
* Returns true if this component's value matches a basic regex "Is this an HTTP URL" test * Returns true if this component's value matches a basic regex "Is this an HTTP URL" test
@@ -20,7 +26,7 @@ export class MetadataRepresentationListElementComponent {
isLink(): boolean { isLink(): boolean {
// Match any string that begins with http:// or https:// // Match any string that begins with http:// or https://
const linkPattern = new RegExp(/^https?\/\/.*/); const linkPattern = new RegExp(/^https?\/\/.*/);
return linkPattern.test(this.metadataRepresentation.getValue()); return linkPattern.test(this.mdRepresentation.getValue());
} }
} }

View File

@@ -1,17 +1,17 @@
<div> <div>
<!-- Because this template is used by default, we will additionally test for representation type and display accordingly --> <!-- Because this template is used by default, we will additionally test for representation type and display accordingly -->
<span *ngIf="(metadataRepresentation.representationType=='plain_text') && !isLink()" class="dont-break-out"> <span *ngIf="(mdRepresentation.representationType=='plain_text') && !isLink()" class="dont-break-out">
{{metadataRepresentation.getValue()}} {{mdRepresentation.getValue()}}
</span> </span>
<a *ngIf="(metadataRepresentation.representationType=='plain_text') && isLink()" class="dont-break-out" <a *ngIf="(mdRepresentation.representationType=='plain_text') && isLink()" class="dont-break-out"
target="_blank" [href]="metadataRepresentation.getValue()"> target="_blank" [href]="mdRepresentation.getValue()">
{{metadataRepresentation.getValue()}} {{mdRepresentation.getValue()}}
</a> </a>
<span *ngIf="(metadataRepresentation.representationType=='authority_controlled')" class="dont-break-out">{{metadataRepresentation.getValue()}}</span> <span *ngIf="(mdRepresentation.representationType=='authority_controlled')" class="dont-break-out">{{mdRepresentation.getValue()}}</span>
<a *ngIf="(metadataRepresentation.representationType=='browse_link')" <a *ngIf="(mdRepresentation.representationType=='browse_link')"
class="dont-break-out ds-browse-link" class="dont-break-out ds-browse-link"
[routerLink]="['/browse/', metadataRepresentation.browseDefinition.id]" [routerLink]="['/browse/', mdRepresentation.browseDefinition.id]"
[queryParams]="getQueryParams()"> [queryParams]="getQueryParams()">
{{metadataRepresentation.getValue()}} {{mdRepresentation.getValue()}}
</a> </a>
</div> </div>

View File

@@ -29,7 +29,7 @@ describe('PlainTextMetadataListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(PlainTextMetadataListElementComponent); fixture = TestBed.createComponent(PlainTextMetadataListElementComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.metadataRepresentation = mockMetadataRepresentation; comp.mdRepresentation = mockMetadataRepresentation;
fixture.detectChanges(); fixture.detectChanges();
})); }));

View File

@@ -20,9 +20,9 @@ export class PlainTextMetadataListElementComponent extends MetadataRepresentatio
* expects 'startsWith' (eg browse by date) or 'value' (eg browse by title) * expects 'startsWith' (eg browse by date) or 'value' (eg browse by title)
*/ */
getQueryParams() { getQueryParams() {
let queryParams = {startsWith: this.metadataRepresentation.getValue()}; let queryParams = {startsWith: this.mdRepresentation.getValue()};
if (this.metadataRepresentation.browseDefinition.metadataBrowse) { if (this.mdRepresentation.browseDefinition.metadataBrowse) {
return {value: this.metadataRepresentation.getValue()}; return {value: this.mdRepresentation.getValue()};
} }
return queryParams; return queryParams;
} }

View File

@@ -19,7 +19,7 @@ import { ObjectCacheService } from '../../../../core/cache/object-cache.service'
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { mergeMap, tap } from 'rxjs/operators'; import { mergeMap, tap } from 'rxjs/operators';
import { isNotEmpty } from '../../../empty.util'; import { isNotEmpty, hasValue } from '../../../empty.util';
import { Context } from '../../../../core/shared/context.model'; import { Context } from '../../../../core/shared/context.model';
@Component({ @Component({
@@ -99,7 +99,9 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
ngOnDestroy() { ngOnDestroy() {
// This ensures the object is removed from cache, when action is performed on task // This ensures the object is removed from cache, when action is performed on task
this.objectCache.remove(this.dso._links.workflowitem.href); if (hasValue(this.dso)) {
this.objectCache.remove(this.dso._links.workflowitem.href);
}
} }
} }

View File

@@ -20,7 +20,7 @@ import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interfac
import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { isNotEmpty } from '../../../empty.util'; import { isNotEmpty, hasValue } from '../../../empty.util';
import { Context } from '../../../../core/shared/context.model'; import { Context } from '../../../../core/shared/context.model';
/** /**
@@ -109,6 +109,8 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
ngOnDestroy() { ngOnDestroy() {
// This ensures the object is removed from cache, when action is performed on task // This ensures the object is removed from cache, when action is performed on task
this.objectCache.remove(this.dso._links.workflowitem.href); if (hasValue(this.dso)) {
this.objectCache.remove(this.dso._links.workflowitem.href);
}
} }
} }

View File

@@ -50,6 +50,7 @@ import { ErrorComponent } from './error/error.component';
import { LoadingComponent } from './loading/loading.component'; import { LoadingComponent } from './loading/loading.component';
import { PaginationComponent } from './pagination/pagination.component'; import { PaginationComponent } from './pagination/pagination.component';
import { ThumbnailComponent } from '../thumbnail/thumbnail.component'; import { ThumbnailComponent } from '../thumbnail/thumbnail.component';
import { ThemedThumbnailComponent } from '../thumbnail/themed-thumbnail.component';
import { SearchFormComponent } from './search-form/search-form.component'; import { SearchFormComponent } from './search-form/search-form.component';
import { ThemedSearchFormComponent } from './search-form/themed-search-form.component'; import { ThemedSearchFormComponent } from './search-form/themed-search-form.component';
import { import {
@@ -348,6 +349,7 @@ const COMPONENTS = [
PageWithSidebarComponent, PageWithSidebarComponent,
SidebarDropdownComponent, SidebarDropdownComponent,
ThumbnailComponent, ThumbnailComponent,
ThemedThumbnailComponent,
MyDSpaceStatusBadgeComponent, MyDSpaceStatusBadgeComponent,
ThemedMyDSpaceStatusBadgeComponent, ThemedMyDSpaceStatusBadgeComponent,
ViewModeSwitchComponent, ViewModeSwitchComponent,

View File

@@ -12,8 +12,8 @@ import {
HostBinding, HostBinding,
ElementRef, ElementRef,
} from '@angular/core'; } from '@angular/core';
import { hasValue, isNotEmpty } from '../empty.util'; import { hasNoValue, hasValue, isNotEmpty } from '../empty.util';
import { from as fromPromise, Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs'; import { combineLatest, from as fromPromise, Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs';
import { ThemeService } from './theme.service'; import { ThemeService } from './theme.service';
import { catchError, switchMap, map, tap } from 'rxjs/operators'; import { catchError, switchMap, map, tap } from 'rxjs/operators';
import { GenericConstructor } from '../../core/shared/generic-constructor'; import { GenericConstructor } from '../../core/shared/generic-constructor';
@@ -35,6 +35,7 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
*/ */
public compRef$: BehaviorSubject<ComponentRef<T>> = new BehaviorSubject(undefined); public compRef$: BehaviorSubject<ComponentRef<T>> = new BehaviorSubject(undefined);
protected lazyLoadObs: Observable<any>;
protected lazyLoadSub: Subscription; protected lazyLoadSub: Subscription;
protected themeSub: Subscription; protected themeSub: Subscription;
@@ -58,20 +59,24 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
protected abstract importUnthemedComponent(): Promise<any>; protected abstract importUnthemedComponent(): Promise<any>;
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
// if an input or output has changed if (hasNoValue(this.compRef)) {
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) { // sometimes the component has not been initialized yet, so it first needs to be initialized
this.connectInputsAndOutputs(); // before being called again
if (this.compRef?.instance && 'ngOnChanges' in this.compRef.instance) { this.initComponentInstance(changes);
(this.compRef.instance as any).ngOnChanges(changes); } else {
// if an input or output has changed
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
this.connectInputsAndOutputs();
if (this.compRef?.instance && 'ngOnChanges' in this.compRef.instance) {
(this.compRef.instance as any).ngOnChanges(changes);
}
} }
} }
} }
ngOnInit(): void { ngOnInit(): void {
this.destroyComponentInstance(); this.destroyComponentInstance();
this.themeSub = this.themeService.getThemeName$().subscribe(() => { this.initComponentInstance();
this.renderComponentInstance();
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@@ -79,33 +84,49 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
this.destroyComponentInstance(); this.destroyComponentInstance();
} }
protected renderComponentInstance(): void { initComponentInstance(changes?: SimpleChanges) {
this.destroyComponentInstance(); this.themeSub = this.themeService?.getThemeName$().subscribe(() => {
this.renderComponentInstance(changes);
});
}
protected renderComponentInstance(changes?: SimpleChanges): void {
if (hasValue(this.lazyLoadSub)) { if (hasValue(this.lazyLoadSub)) {
this.lazyLoadSub.unsubscribe(); this.lazyLoadSub.unsubscribe();
} }
this.lazyLoadSub = this.resolveThemedComponent(this.themeService.getThemeName()).pipe( if (hasNoValue(this.lazyLoadObs)) {
switchMap((themedFile: any) => { this.destroyComponentInstance();
if (hasValue(themedFile) && hasValue(themedFile[this.getComponentName()])) {
// if the file is not null, and exports a component with the specified name, this.lazyLoadObs = combineLatest([
// return that component observableOf(changes),
return [themedFile[this.getComponentName()]]; this.resolveThemedComponent(this.themeService.getThemeName()).pipe(
} else { switchMap((themedFile: any) => {
// otherwise import and return the default component if (hasValue(themedFile) && hasValue(themedFile[this.getComponentName()])) {
return fromPromise(this.importUnthemedComponent()).pipe( // if the file is not null, and exports a component with the specified name,
// return that component
return [themedFile[this.getComponentName()]];
} else {
// otherwise import and return the default component
return fromPromise(this.importUnthemedComponent()).pipe(
tap(() => this.usedTheme = BASE_THEME_NAME), tap(() => this.usedTheme = BASE_THEME_NAME),
map((unthemedFile: any) => { map((unthemedFile: any) => {
return unthemedFile[this.getComponentName()]; return unthemedFile[this.getComponentName()];
}) })
); );
} }
}), })),
).subscribe((constructor: GenericConstructor<T>) => { ]);
}
this.lazyLoadSub = this.lazyLoadObs.subscribe(([simpleChanges, constructor]: [SimpleChanges, GenericConstructor<T>]) => {
const factory = this.resolver.resolveComponentFactory(constructor); const factory = this.resolver.resolveComponentFactory(constructor);
this.compRef = this.vcr.createComponent(factory, undefined, undefined, [this.themedElementContent.nativeElement.childNodes]); this.compRef = this.vcr.createComponent(factory, undefined, undefined, [this.themedElementContent.nativeElement.childNodes]);
this.connectInputsAndOutputs(); if (hasValue(simpleChanges)) {
this.ngOnChanges(simpleChanges);
} else {
this.connectInputsAndOutputs();
}
this.compRef$.next(this.compRef); this.compRef$.next(this.compRef);
this.cdr.markForCheck(); this.cdr.markForCheck();
this.themedElementContent.nativeElement.remove(); this.themedElementContent.nativeElement.remove();

View File

@@ -1,8 +1,8 @@
<ng-container *ngIf="fileData"> <ng-container *ngIf="fileData">
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-2">
<!--ds-thumbnail [thumbnail]="bitstreamsList[bitstreamKey].url | async"></ds-thumbnail--> <!--ds-themed-thumbnail [thumbnail]="bitstreamsList[bitstreamKey].url | async"></ds-themed-thumbnail-->
<ds-thumbnail [thumbnail]="fileData?.thumbnail"></ds-thumbnail> <ds-themed-thumbnail [thumbnail]="fileData?.thumbnail"></ds-themed-thumbnail>
</div> </div>
<div class="col-md-10"> <div class="col-md-10">
<div class="float-left w-75"> <div class="float-left w-75">

View File

@@ -0,0 +1,44 @@
import { ThemedComponent } from '../shared/theme-support/themed.component';
import { Component, Input } from '@angular/core';
import { ThumbnailComponent } from './thumbnail.component';
import { Bitstream } from '../core/shared/bitstream.model';
import { RemoteData } from '../core/data/remote-data';
@Component({
selector: 'ds-themed-thumbnail',
styleUrls: [],
templateUrl: '../shared/theme-support/themed.component.html',
})
export class ThemedThumbnailComponent extends ThemedComponent<ThumbnailComponent> {
@Input() thumbnail: Bitstream | RemoteData<Bitstream>;
@Input() defaultImage?: string | null;
@Input() alt?: string;
@Input() placeholder?: string;
@Input() limitWidth?: boolean;
protected inAndOutputNames: (keyof ThumbnailComponent & keyof this)[] = [
'thumbnail',
'defaultImage',
'alt',
'placeholder',
'limitWidth',
];
protected getComponentName(): string {
return 'ThumbnailComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../themes/${themeName}/app/thumbnail/thumbnail.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./thumbnail.component');
}
}

View File

@@ -134,7 +134,7 @@ describe('ThumbnailComponent', () => {
const img = fixture.debugElement.query(By.css('img.thumbnail-content')); const img = fixture.debugElement.query(By.css('img.thumbnail-content'));
img.nativeNode.onerror = null; img.nativeNode.onerror = null;
comp.ngOnChanges(); comp.ngOnChanges({});
setSrcSpy = spyOn(comp, 'setSrc').and.callThrough(); setSrcSpy = spyOn(comp, 'setSrc').and.callThrough();
}); });
@@ -261,14 +261,14 @@ describe('ThumbnailComponent', () => {
describe('if content can be loaded', () => { describe('if content can be loaded', () => {
it('should display an image', () => { it('should display an image', () => {
comp.ngOnChanges(); comp.ngOnChanges({});
fixture.detectChanges(); fixture.detectChanges();
const image: HTMLElement = fixture.debugElement.query(By.css('img')).nativeElement; const image: HTMLElement = fixture.debugElement.query(By.css('img')).nativeElement;
expect(image.getAttribute('src')).toBe(thumbnail._links.content.href); expect(image.getAttribute('src')).toBe(thumbnail._links.content.href);
}); });
it('should include the alt text', () => { it('should include the alt text', () => {
comp.ngOnChanges(); comp.ngOnChanges({});
fixture.detectChanges(); fixture.detectChanges();
const image: HTMLElement = fixture.debugElement.query(By.css('img')).nativeElement; const image: HTMLElement = fixture.debugElement.query(By.css('img')).nativeElement;
expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt);
@@ -301,14 +301,14 @@ describe('ThumbnailComponent', () => {
describe('if content can be loaded', () => { describe('if content can be loaded', () => {
it('should display an image', () => { it('should display an image', () => {
comp.ngOnChanges(); comp.ngOnChanges({});
fixture.detectChanges(); fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement; const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('src')).toBe(thumbnail._links.content.href); expect(image.getAttribute('src')).toBe(thumbnail._links.content.href);
}); });
it('should display the alt text', () => { it('should display the alt text', () => {
comp.ngOnChanges(); comp.ngOnChanges({});
fixture.detectChanges(); fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement; const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt);
@@ -327,7 +327,7 @@ describe('ThumbnailComponent', () => {
it('should show the default image', () => { it('should show the default image', () => {
comp.defaultImage = 'default/image.jpg'; comp.defaultImage = 'default/image.jpg';
comp.ngOnChanges(); comp.ngOnChanges({});
expect(comp.src$.getValue()).toBe('default/image.jpg'); expect(comp.src$.getValue()).toBe('default/image.jpg');
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Bitstream } from '../core/shared/bitstream.model'; import { Bitstream } from '../core/shared/bitstream.model';
import { hasNoValue, hasValue } from '../shared/empty.util'; import { hasNoValue, hasValue } from '../shared/empty.util';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
@@ -70,8 +70,9 @@ export class ThumbnailComponent implements OnChanges {
* Resolve the thumbnail. * Resolve the thumbnail.
* Use a default image if no actual image is available. * Use a default image if no actual image is available.
*/ */
ngOnChanges(): void { ngOnChanges(changes: SimpleChanges): void {
if (hasNoValue(this.thumbnail)) { if (hasNoValue(this.thumbnail)) {
this.setSrc(this.defaultImage);
return; return;
} }

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { ThumbnailComponent as BaseComponent } from '../../../../app/thumbnail/thumbnail.component';
@Component({
selector: 'ds-thumbnail',
// styleUrls: ['./thumbnail.component.scss'],
styleUrls: ['../../../../app/thumbnail/thumbnail.component.scss'],
// templateUrl: './thumbnail.component.html',
templateUrl: '../../../../app/thumbnail/thumbnail.component.html',
})
export class ThumbnailComponent extends BaseComponent {
}

View File

@@ -140,6 +140,7 @@ import {
} from './app/item-page/media-viewer/media-viewer-video/media-viewer-video.component'; } from './app/item-page/media-viewer/media-viewer-video/media-viewer-video.component';
import { NgxGalleryModule } from '@kolkov/ngx-gallery'; import { NgxGalleryModule } from '@kolkov/ngx-gallery';
import { WorkspaceItemsDeletePageComponent } from './app/workspace-items-delete-page/workspace-items-delete/workspace-items-delete.component'; import { WorkspaceItemsDeletePageComponent } from './app/workspace-items-delete-page/workspace-items-delete/workspace-items-delete.component';
import { ThumbnailComponent } from './app/thumbnail/thumbnail.component';
const DECLARATIONS = [ const DECLARATIONS = [
FileSectionComponent, FileSectionComponent,
@@ -215,6 +216,7 @@ const DECLARATIONS = [
MediaViewerImageComponent, MediaViewerImageComponent,
MediaViewerVideoComponent, MediaViewerVideoComponent,
WorkspaceItemsDeletePageComponent, WorkspaceItemsDeletePageComponent,
ThumbnailComponent,
]; ];
@NgModule({ @NgModule({