mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into coar-notify-7
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<ng-template dsListableObject>
|
||||
<ng-template dsDynamicComponentLoader>
|
||||
</ng-template>
|
||||
<div #badges>
|
||||
<ng-content></ng-content>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, ComponentRef, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import {
|
||||
@@ -11,9 +11,10 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||
import { DynamicComponentLoaderDirective } from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
|
||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { hasValue } from '../../../../../shared/empty.util';
|
||||
|
||||
@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||
@Component({
|
||||
@@ -24,17 +25,18 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service
|
||||
/**
|
||||
* The component for displaying a list element for an item search result on the admin search page
|
||||
*/
|
||||
export class ItemAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<ItemSearchResult, Item> implements OnInit {
|
||||
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
|
||||
export class ItemAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<ItemSearchResult, Item> implements OnDestroy, OnInit {
|
||||
@ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective;
|
||||
@ViewChild('badges', { static: true }) badges: ElementRef;
|
||||
@ViewChild('buttons', { static: true }) buttons: ElementRef;
|
||||
|
||||
protected compRef: ComponentRef<Component>;
|
||||
|
||||
constructor(
|
||||
public dsoNameService: DSONameService,
|
||||
protected truncatableService: TruncatableService,
|
||||
protected bitstreamDataService: BitstreamDataService,
|
||||
private themeService: ThemeService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
) {
|
||||
super(dsoNameService, truncatableService, bitstreamDataService);
|
||||
}
|
||||
@@ -44,23 +46,32 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
|
||||
const component: GenericConstructor<Component> = this.getComponent();
|
||||
|
||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||
const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
const componentRef = viewContainerRef.createComponent(
|
||||
componentFactory,
|
||||
0,
|
||||
undefined,
|
||||
[
|
||||
[this.badges.nativeElement],
|
||||
[this.buttons.nativeElement]
|
||||
]);
|
||||
(componentRef.instance as any).object = this.object;
|
||||
(componentRef.instance as any).index = this.index;
|
||||
(componentRef.instance as any).linkType = this.linkType;
|
||||
(componentRef.instance as any).listID = this.listID;
|
||||
this.compRef = viewContainerRef.createComponent(
|
||||
component, {
|
||||
index: 0,
|
||||
injector: undefined,
|
||||
projectableNodes: [
|
||||
[this.badges.nativeElement],
|
||||
[this.buttons.nativeElement],
|
||||
],
|
||||
},
|
||||
);
|
||||
this.compRef.setInput('object',this.object);
|
||||
this.compRef.setInput('index', this.index);
|
||||
this.compRef.setInput('linkType', this.linkType);
|
||||
this.compRef.setInput('listID', this.listID);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.compRef)) {
|
||||
this.compRef.destroy();
|
||||
this.compRef = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ng-template dsListableObject>
|
||||
<ng-template dsDynamicComponentLoader>
|
||||
</ng-template>
|
||||
<div #badges class="position-absolute ml-1">
|
||||
<div class="workflow-badge">
|
||||
|
@@ -18,8 +18,8 @@ import {
|
||||
ItemGridElementComponent
|
||||
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
|
||||
import {
|
||||
ListableObjectDirective
|
||||
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||
DynamicComponentLoaderDirective
|
||||
} from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
|
||||
import {
|
||||
WorkflowItemSearchResult
|
||||
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||
@@ -38,7 +38,7 @@ describe('WorkflowItemSearchResultAdminWorkflowGridElementComponent', () => {
|
||||
let itemRD$;
|
||||
let linkService;
|
||||
let object;
|
||||
let themeService;
|
||||
let themeService: ThemeService;
|
||||
|
||||
function init() {
|
||||
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
|
||||
@@ -55,7 +55,11 @@ describe('WorkflowItemSearchResultAdminWorkflowGridElementComponent', () => {
|
||||
init();
|
||||
TestBed.configureTestingModule(
|
||||
{
|
||||
declarations: [WorkflowItemSearchResultAdminWorkflowGridElementComponent, ItemGridElementComponent, ListableObjectDirective],
|
||||
declarations: [
|
||||
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
||||
ItemGridElementComponent,
|
||||
DynamicComponentLoaderDirective,
|
||||
],
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Component, ElementRef, ViewChild, ComponentRef, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import {
|
||||
@@ -10,7 +10,7 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||
import { DynamicComponentLoaderDirective } from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
|
||||
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||
@@ -24,6 +24,7 @@ import { take } from 'rxjs/operators';
|
||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { hasValue } from '../../../../../shared/empty.util';
|
||||
|
||||
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
|
||||
@Component({
|
||||
@@ -34,11 +35,11 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service
|
||||
/**
|
||||
* The component for displaying a grid element for an workflow item on the admin workflow search page
|
||||
*/
|
||||
export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkflowItemSearchResult, WorkflowItem> {
|
||||
export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkflowItemSearchResult, WorkflowItem> implements OnDestroy, OnInit {
|
||||
/**
|
||||
* Directive used to render the dynamic component in
|
||||
*/
|
||||
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
|
||||
@ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective;
|
||||
|
||||
/**
|
||||
* The html child that contains the badges html
|
||||
@@ -55,9 +56,10 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
|
||||
*/
|
||||
public item$: Observable<Item>;
|
||||
|
||||
protected compRef: ComponentRef<Component>;
|
||||
|
||||
constructor(
|
||||
public dsoNameService: DSONameService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private linkService: LinkService,
|
||||
protected truncatableService: TruncatableService,
|
||||
private themeService: ThemeService,
|
||||
@@ -75,26 +77,34 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
|
||||
this.dso = this.linkService.resolveLink(this.dso, followLink('item'));
|
||||
this.item$ = (this.dso.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload());
|
||||
this.item$.pipe(take(1)).subscribe((item: Item) => {
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item));
|
||||
const component: GenericConstructor<Component> = this.getComponent(item);
|
||||
|
||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
const componentRef = viewContainerRef.createComponent(
|
||||
componentFactory,
|
||||
0,
|
||||
undefined,
|
||||
[
|
||||
this.compRef = viewContainerRef.createComponent(
|
||||
component, {
|
||||
index: 0,
|
||||
injector: undefined,
|
||||
projectableNodes: [
|
||||
[this.badges.nativeElement],
|
||||
[this.buttons.nativeElement]
|
||||
]);
|
||||
(componentRef.instance as any).object = item;
|
||||
(componentRef.instance as any).index = this.index;
|
||||
(componentRef.instance as any).linkType = this.linkType;
|
||||
(componentRef.instance as any).listID = this.listID;
|
||||
componentRef.changeDetectorRef.detectChanges();
|
||||
}
|
||||
);
|
||||
[this.buttons.nativeElement],
|
||||
],
|
||||
},
|
||||
);
|
||||
this.compRef.setInput('object', item);
|
||||
this.compRef.setInput('index', this.index);
|
||||
this.compRef.setInput('linkType', this.linkType);
|
||||
this.compRef.setInput('listID', this.listID);
|
||||
this.compRef.changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.compRef)) {
|
||||
this.compRef.destroy();
|
||||
this.compRef = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ng-template dsListableObject>
|
||||
<ng-template dsDynamicComponentLoader>
|
||||
</ng-template>
|
||||
<div #badges class="position-absolute ml-1">
|
||||
<div class="workflow-badge">
|
||||
|
@@ -20,8 +20,8 @@ import {
|
||||
ItemGridElementComponent
|
||||
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
|
||||
import {
|
||||
ListableObjectDirective
|
||||
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||
DynamicComponentLoaderDirective
|
||||
} from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
|
||||
import {
|
||||
WorkflowItemSearchResult
|
||||
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||
@@ -45,7 +45,7 @@ describe('WorkspaceItemSearchResultAdminWorkflowGridElementComponent', () => {
|
||||
let itemRD$;
|
||||
let linkService;
|
||||
let object;
|
||||
let themeService;
|
||||
let themeService: ThemeService;
|
||||
let supervisionOrderDataService;
|
||||
|
||||
function init() {
|
||||
@@ -67,7 +67,11 @@ describe('WorkspaceItemSearchResultAdminWorkflowGridElementComponent', () => {
|
||||
init();
|
||||
TestBed.configureTestingModule(
|
||||
{
|
||||
declarations: [WorkspaceItemSearchResultAdminWorkflowGridElementComponent, ItemGridElementComponent, ListableObjectDirective],
|
||||
declarations: [
|
||||
WorkspaceItemSearchResultAdminWorkflowGridElementComponent,
|
||||
ItemGridElementComponent,
|
||||
DynamicComponentLoaderDirective,
|
||||
],
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentFactoryResolver, ElementRef, ViewChild, OnInit } from '@angular/core';
|
||||
import { Component, ElementRef, ViewChild, OnInit, OnDestroy, ComponentRef } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map, mergeMap, take, tap } from 'rxjs/operators';
|
||||
@@ -16,9 +16,7 @@ import {
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||
import {
|
||||
ListableObjectDirective
|
||||
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||
import { DynamicComponentLoaderDirective } from '../../../../../shared/abstract-component-loader/dynamic-component-loader.directive';
|
||||
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||
@@ -37,6 +35,7 @@ import { SupervisionOrder } from '../../../../../core/supervision-order/models/s
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { hasValue } from '../../../../../shared/empty.util';
|
||||
|
||||
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
|
||||
@Component({
|
||||
@@ -47,7 +46,7 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service
|
||||
/**
|
||||
* The component for displaying a grid element for an workflow item on the admin workflow search page
|
||||
*/
|
||||
export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkspaceItemSearchResult, WorkspaceItem> implements OnInit {
|
||||
export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkspaceItemSearchResult, WorkspaceItem> implements OnDestroy, OnInit {
|
||||
|
||||
/**
|
||||
* The item linked to the workspace item
|
||||
@@ -67,7 +66,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
|
||||
/**
|
||||
* Directive used to render the dynamic component in
|
||||
*/
|
||||
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
|
||||
@ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective;
|
||||
|
||||
/**
|
||||
* The html child that contains the badges html
|
||||
@@ -79,9 +78,13 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
|
||||
*/
|
||||
@ViewChild('buttons', { static: true }) buttons: ElementRef;
|
||||
|
||||
/**
|
||||
* The reference to the dynamic component
|
||||
*/
|
||||
protected compRef: ComponentRef<Component>;
|
||||
|
||||
constructor(
|
||||
public dsoNameService: DSONameService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private linkService: LinkService,
|
||||
protected truncatableService: TruncatableService,
|
||||
private themeService: ThemeService,
|
||||
@@ -100,24 +103,24 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
|
||||
this.dso = this.linkService.resolveLink(this.dso, followLink('item'));
|
||||
this.item$ = (this.dso.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload());
|
||||
this.item$.pipe(take(1)).subscribe((item: Item) => {
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item));
|
||||
const component: GenericConstructor<Component> = this.getComponent(item);
|
||||
|
||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||
const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
const componentRef = viewContainerRef.createComponent(
|
||||
componentFactory,
|
||||
0,
|
||||
undefined,
|
||||
[
|
||||
this.compRef = viewContainerRef.createComponent(
|
||||
component, {
|
||||
index: 0,
|
||||
projectableNodes: [
|
||||
[this.badges.nativeElement],
|
||||
[this.buttons.nativeElement]
|
||||
]);
|
||||
(componentRef.instance as any).object = item;
|
||||
(componentRef.instance as any).index = this.index;
|
||||
(componentRef.instance as any).linkType = this.linkType;
|
||||
(componentRef.instance as any).listID = this.listID;
|
||||
componentRef.changeDetectorRef.detectChanges();
|
||||
],
|
||||
});
|
||||
this.compRef.setInput('object', item);
|
||||
this.compRef.setInput('index', this.index);
|
||||
this.compRef.setInput('linkType', this.linkType);
|
||||
this.compRef.setInput('listID', this.listID);
|
||||
this.compRef.changeDetectorRef.detectChanges();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -130,6 +133,13 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.compRef)) {
|
||||
this.compRef.destroy();
|
||||
this.compRef = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the component depending on the item's entity type, view mode and context
|
||||
* @returns {GenericConstructor<Component>}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core';
|
||||
import {
|
||||
BrowseByMetadataPageComponent,
|
||||
browseParamsToOptions,
|
||||
@@ -19,6 +19,8 @@ import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
||||
import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-date-page',
|
||||
@@ -30,7 +32,8 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
* A metadata definition (a.k.a. browse id) is a short term used to describe one or multiple metadata fields.
|
||||
* An example would be 'dateissued' for 'dc.date.issued'
|
||||
*/
|
||||
export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
@rendersBrowseBy(BrowseByDataType.Date)
|
||||
export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The default metadata keys to use for determining the lower limit of the StartsWith dropdown options
|
||||
|
@@ -1,29 +0,0 @@
|
||||
import {Component} from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { BrowseByDatePageComponent } from './browse-by-date-page.component';
|
||||
import {BrowseByDataType, rendersBrowseBy} from '../browse-by-switcher/browse-by-decorator';
|
||||
|
||||
/**
|
||||
* Themed wrapper for BrowseByDatePageComponent
|
||||
* */
|
||||
@Component({
|
||||
selector: 'ds-themed-browse-by-metadata-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
|
||||
@rendersBrowseBy(BrowseByDataType.Date)
|
||||
export class ThemedBrowseByDatePageComponent
|
||||
extends ThemedComponent<BrowseByDatePageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'BrowseByDatePageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/browse-by/browse-by-date-page/browse-by-date-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./browse-by-date-page.component`);
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@ import { first } from 'rxjs/operators';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';
|
||||
import { BrowseByDataType } from './browse-by-switcher/browse-by-data-type';
|
||||
import { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model';
|
||||
import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock';
|
||||
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { Component, Inject, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, Inject, OnInit, OnDestroy, Input } from '@angular/core';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
@@ -22,6 +22,9 @@ import { Collection } from '../../core/shared/collection.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
||||
import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type';
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
|
||||
export const BBM_PAGINATION_ID = 'bbm';
|
||||
|
||||
@@ -36,8 +39,19 @@ export const BBM_PAGINATION_ID = 'bbm';
|
||||
* or multiple metadata fields. An example would be 'author' for
|
||||
* 'dc.contributor.*'
|
||||
*/
|
||||
@rendersBrowseBy(BrowseByDataType.Metadata)
|
||||
export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* The optional context
|
||||
*/
|
||||
@Input() context: Context;
|
||||
|
||||
/**
|
||||
* The {@link BrowseByDataType} of this Component
|
||||
*/
|
||||
@Input() browseByType: BrowseByDataType;
|
||||
|
||||
/**
|
||||
* The list of browse-entries to display
|
||||
*/
|
||||
@@ -130,7 +144,6 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
@Inject(APP_CONFIG) public appConfig: AppConfig,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
|
||||
this.fetchThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
|
||||
id: BBM_PAGINATION_ID,
|
||||
@@ -278,7 +291,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
this.subs.filter((sub: Subscription) => hasValue(sub)).forEach((sub: Subscription) => sub.unsubscribe());
|
||||
this.paginationService.clearPagination(this.paginationConfig.id);
|
||||
}
|
||||
|
||||
|
@@ -1,29 +0,0 @@
|
||||
import {Component} from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page.component';
|
||||
import {BrowseByDataType, rendersBrowseBy} from '../browse-by-switcher/browse-by-decorator';
|
||||
|
||||
/**
|
||||
* Themed wrapper for BrowseByMetadataPageComponent
|
||||
**/
|
||||
@Component({
|
||||
selector: 'ds-themed-browse-by-metadata-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
|
||||
@rendersBrowseBy(BrowseByDataType.Metadata)
|
||||
export class ThemedBrowseByMetadataPageComponent
|
||||
extends ThemedComponent<BrowseByMetadataPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'BrowseByMetadataPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./browse-by-metadata-page.component`);
|
||||
}
|
||||
}
|
@@ -5,12 +5,19 @@ import { ItemDataService } from '../core/data/item-data.service';
|
||||
import { BrowseService } from '../core/browse/browse.service';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||
import { BrowseByPageComponent } from './browse-by-page/browse-by-page.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
const DECLARATIONS = [
|
||||
BrowseByPageComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedBrowseByModule,
|
||||
BrowseByRoutingModule,
|
||||
BrowseByModule.withEntryComponents(),
|
||||
BrowseByModule,
|
||||
SharedModule,
|
||||
],
|
||||
providers: [
|
||||
ItemDataService,
|
||||
@@ -18,8 +25,11 @@ import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.modul
|
||||
BrowseByGuard,
|
||||
],
|
||||
declarations: [
|
||||
|
||||
]
|
||||
...DECLARATIONS,
|
||||
],
|
||||
exports: [
|
||||
...DECLARATIONS,
|
||||
],
|
||||
})
|
||||
export class BrowseByPageModule {
|
||||
|
||||
|
@@ -0,0 +1,2 @@
|
||||
<ds-browse-by-switcher [browseByType]="browseByType$ | async">
|
||||
</ds-browse-by-switcher>
|
@@ -0,0 +1,69 @@
|
||||
// eslint-disable-next-line max-classes-per-file
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BrowseByPageComponent } from './browse-by-page.component';
|
||||
import { BrowseBySwitcherComponent } from '../browse-by-switcher/browse-by-switcher.component';
|
||||
import { DynamicComponentLoaderDirective } from '../../shared/abstract-component-loader/dynamic-component-loader.directive';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
||||
import { Component } from '@angular/core';
|
||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type';
|
||||
|
||||
@rendersBrowseBy('BrowseByPageComponent' as BrowseByDataType)
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: '',
|
||||
template: '<span id="BrowseByTestComponent"></span>',
|
||||
})
|
||||
class BrowseByTestComponent {
|
||||
}
|
||||
|
||||
class TestBrowseByPageBrowseDefinition extends BrowseDefinition {
|
||||
getRenderType(): BrowseByDataType {
|
||||
return 'BrowseByPageComponent' as BrowseByDataType;
|
||||
}
|
||||
}
|
||||
|
||||
describe('BrowseByPageComponent', () => {
|
||||
let component: BrowseByPageComponent;
|
||||
let fixture: ComponentFixture<BrowseByPageComponent>;
|
||||
|
||||
let activatedRoute: ActivatedRouteStub;
|
||||
let themeService: ThemeService;
|
||||
|
||||
beforeEach(async () => {
|
||||
activatedRoute = new ActivatedRouteStub();
|
||||
themeService = getMockThemeService();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
BrowseByPageComponent,
|
||||
BrowseBySwitcherComponent,
|
||||
DynamicComponentLoaderDirective,
|
||||
],
|
||||
providers: [
|
||||
BrowseByTestComponent,
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BrowseByPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create the correct browse section based on the route browseDefinition', () => {
|
||||
activatedRoute.testData = {
|
||||
browseDefinition: new TestBrowseByPageBrowseDefinition(),
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
expect(fixture.debugElement.query(By.css('#BrowseByTestComponent'))).not.toBeNull();
|
||||
});
|
||||
});
|
31
src/app/browse-by/browse-by-page/browse-by-page.component.ts
Normal file
31
src/app/browse-by/browse-by-page/browse-by-page.component.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-page',
|
||||
templateUrl: './browse-by-page.component.html',
|
||||
styleUrls: ['./browse-by-page.component.scss'],
|
||||
})
|
||||
export class BrowseByPageComponent implements OnInit {
|
||||
|
||||
browseByType$: Observable<BrowseByDataType>;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the correct browse-by component by using the relevant config from the route data
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.browseByType$ = this.route.data.pipe(
|
||||
map((data: { browseDefinition: BrowseDefinition }) => data.browseDefinition.getRenderType()),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -3,7 +3,7 @@ import { NgModule } from '@angular/core';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
|
||||
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
|
||||
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
|
||||
import { BrowseByPageComponent } from './browse-by-page/browse-by-page.component';
|
||||
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
||||
|
||||
@NgModule({
|
||||
@@ -18,7 +18,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: ThemedBrowseBySwitcherComponent,
|
||||
component: BrowseByPageComponent,
|
||||
canActivate: [BrowseByGuard],
|
||||
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver },
|
||||
data: { title: 'browse.title.page', breadcrumbKey: 'browse.metadata' }
|
||||
|
@@ -0,0 +1,6 @@
|
||||
export enum BrowseByDataType {
|
||||
Title = 'title',
|
||||
Metadata = 'text',
|
||||
Date = 'date',
|
||||
Hierarchy = 'hierarchy',
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
import { BrowseByDataType, rendersBrowseBy } from './browse-by-decorator';
|
||||
import { BrowseByDataType } from './browse-by-data-type';
|
||||
import { rendersBrowseBy } from './browse-by-decorator';
|
||||
|
||||
describe('BrowseByDecorator', () => {
|
||||
const titleDecorator = rendersBrowseBy(BrowseByDataType.Title);
|
||||
|
@@ -1,40 +1,36 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { hasNoValue } from '../../shared/empty.util';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { DEFAULT_THEME, resolveTheme } from '../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import {
|
||||
DEFAULT_THEME,
|
||||
resolveTheme
|
||||
} from '../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
|
||||
export enum BrowseByDataType {
|
||||
Title = 'title',
|
||||
Metadata = 'text',
|
||||
Date = 'date'
|
||||
}
|
||||
import { BrowseByDataType } from './browse-by-data-type';
|
||||
|
||||
export const DEFAULT_BROWSE_BY_TYPE = BrowseByDataType.Metadata;
|
||||
export const DEFAULT_BROWSE_BY_CONTEXT = Context.Any;
|
||||
|
||||
export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType, theme) => GenericConstructor<any>>('getComponentByBrowseByType', {
|
||||
providedIn: 'root',
|
||||
factory: () => getComponentByBrowseByType
|
||||
});
|
||||
|
||||
const map = new Map();
|
||||
const map: Map<BrowseByDataType, Map<Context, Map<string, GenericConstructor<Component>>>> = new Map();
|
||||
|
||||
/**
|
||||
* Decorator used for rendering Browse-By pages by type
|
||||
* @param browseByType The type of page
|
||||
* @param context The optional context for the component
|
||||
* @param theme The optional theme for the component
|
||||
*/
|
||||
export function rendersBrowseBy(browseByType: string, theme = DEFAULT_THEME) {
|
||||
export function rendersBrowseBy(browseByType: BrowseByDataType, context = DEFAULT_BROWSE_BY_CONTEXT, theme = DEFAULT_THEME) {
|
||||
return function decorator(component: any) {
|
||||
if (hasNoValue(browseByType)) {
|
||||
return;
|
||||
}
|
||||
if (hasNoValue(map.get(browseByType))) {
|
||||
map.set(browseByType, new Map());
|
||||
}
|
||||
if (hasNoValue(map.get(browseByType).get(theme))) {
|
||||
map.get(browseByType).set(theme, component);
|
||||
if (hasNoValue(map.get(browseByType).get(context))) {
|
||||
map.get(browseByType).set(context, new Map());
|
||||
}
|
||||
if (hasNoValue(map.get(browseByType).get(context).get(theme))) {
|
||||
map.get(browseByType).get(context).set(theme, component);
|
||||
} else {
|
||||
throw new Error(`There can't be more than one component to render Browse-By of type "${browseByType}" and theme "${theme}"`);
|
||||
throw new Error(`There can't be more than one component to render Browse-By of type "${browseByType}", context "${context}" and theme "${theme}"`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -42,12 +38,17 @@ export function rendersBrowseBy(browseByType: string, theme = DEFAULT_THEME) {
|
||||
/**
|
||||
* Get the component used for rendering a Browse-By page by type
|
||||
* @param browseByType The type of page
|
||||
* @param context The context to match
|
||||
* @param theme the theme to match
|
||||
*/
|
||||
export function getComponentByBrowseByType(browseByType, theme) {
|
||||
let themeMap = map.get(browseByType);
|
||||
export function getComponentByBrowseByType(browseByType: BrowseByDataType, context: Context, theme: string): GenericConstructor<Component> {
|
||||
let contextMap: Map<Context, Map<string, GenericConstructor<Component>>> = map.get(browseByType);
|
||||
if (hasNoValue(contextMap)) {
|
||||
contextMap = map.get(DEFAULT_BROWSE_BY_TYPE);
|
||||
}
|
||||
let themeMap: Map<string, GenericConstructor<Component>> = contextMap.get(context);
|
||||
if (hasNoValue(themeMap)) {
|
||||
themeMap = map.get(DEFAULT_BROWSE_BY_TYPE);
|
||||
themeMap = contextMap.get(DEFAULT_BROWSE_BY_CONTEXT);
|
||||
}
|
||||
const comp = resolveTheme(themeMap, theme);
|
||||
if (hasNoValue(comp)) {
|
||||
|
@@ -1 +0,0 @@
|
||||
<ng-container *ngComponentOutlet="browseByComponent | async"></ng-container>
|
@@ -1,13 +1,23 @@
|
||||
import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { SimpleChange, Component } from '@angular/core';
|
||||
import { rendersBrowseBy } from './browse-by-decorator';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
import { FlatBrowseDefinition } from '../../core/shared/flat-browse-definition.model';
|
||||
import { ValueListBrowseDefinition } from '../../core/shared/value-list-browse-definition.model';
|
||||
import { NonHierarchicalBrowseDefinition } from '../../core/shared/non-hierarchical-browse-definition';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
import { DynamicComponentLoaderDirective } from '../../shared/abstract-component-loader/dynamic-component-loader.directive';
|
||||
import { BrowseByDataType } from './browse-by-data-type';
|
||||
|
||||
@rendersBrowseBy('BrowseBySwitcherComponent' as BrowseByDataType)
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: '',
|
||||
template: '<span id="BrowseByTestComponent"></span>',
|
||||
})
|
||||
class BrowseByTestComponent {
|
||||
}
|
||||
|
||||
describe('BrowseBySwitcherComponent', () => {
|
||||
let comp: BrowseBySwitcherComponent;
|
||||
@@ -41,46 +51,45 @@ describe('BrowseBySwitcherComponent', () => {
|
||||
),
|
||||
];
|
||||
|
||||
const data = new BehaviorSubject(createDataWithBrowseDefinition(new FlatBrowseDefinition()));
|
||||
|
||||
const activatedRouteStub = {
|
||||
data
|
||||
};
|
||||
|
||||
let themeService: ThemeService;
|
||||
let themeName: string;
|
||||
const themeName = 'dspace';
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
themeName = 'dspace';
|
||||
themeService = jasmine.createSpyObj('themeService', {
|
||||
getThemeName: themeName,
|
||||
});
|
||||
themeService = getMockThemeService(themeName);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BrowseBySwitcherComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
{ provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) }
|
||||
void TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
BrowseBySwitcherComponent,
|
||||
DynamicComponentLoaderDirective,
|
||||
],
|
||||
providers: [
|
||||
BrowseByTestComponent,
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(BrowseBySwitcherComponent);
|
||||
comp = fixture.componentInstance;
|
||||
spyOn(comp, 'getComponent').and.returnValue(BrowseByTestComponent);
|
||||
spyOn(comp, 'connectInputsAndOutputs').and.callThrough();
|
||||
}));
|
||||
|
||||
types.forEach((type: NonHierarchicalBrowseDefinition) => {
|
||||
describe(`when switching to a browse-by page for "${type.id}"`, () => {
|
||||
beforeEach(() => {
|
||||
data.next(createDataWithBrowseDefinition(type));
|
||||
beforeEach(async () => {
|
||||
comp.browseByType = type.dataType;
|
||||
comp.ngOnChanges({
|
||||
browseByType: new SimpleChange(undefined, type.dataType, true),
|
||||
});
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it(`should call getComponentByBrowseByType with type "${type.dataType}"`, () => {
|
||||
expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.dataType, themeName);
|
||||
it(`should call getComponent with type "${type.dataType}"`, () => {
|
||||
expect(comp.getComponent).toHaveBeenCalled();
|
||||
expect(comp.connectInputsAndOutputs).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,38 +1,32 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { getComponentByBrowseByType } from './browse-by-decorator';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
import { AbstractComponentLoaderComponent } from '../../shared/abstract-component-loader/abstract-component-loader.component';
|
||||
import { BrowseByDataType } from './browse-by-data-type';
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-switcher',
|
||||
templateUrl: './browse-by-switcher.component.html'
|
||||
templateUrl: '../../shared/abstract-component-loader/abstract-component-loader.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for determining what Browse-By component to use depending on the metadata (browse ID) provided
|
||||
*/
|
||||
export class BrowseBySwitcherComponent implements OnInit {
|
||||
export class BrowseBySwitcherComponent extends AbstractComponentLoaderComponent<Component> {
|
||||
|
||||
/**
|
||||
* Resolved browse-by component
|
||||
*/
|
||||
browseByComponent: Observable<any>;
|
||||
@Input() context: Context;
|
||||
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
protected themeService: ThemeService,
|
||||
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor<any>) {
|
||||
}
|
||||
@Input() browseByType: BrowseByDataType;
|
||||
|
||||
/**
|
||||
* Fetch the correct browse-by component by using the relevant config from the route data
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.browseByComponent = this.route.data.pipe(
|
||||
map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName()))
|
||||
);
|
||||
protected inputNamesDependentForComponent: (keyof this & string)[] = [
|
||||
'context',
|
||||
'browseByType',
|
||||
];
|
||||
|
||||
protected inputNames: (keyof this & string)[] = [
|
||||
'context',
|
||||
'browseByType',
|
||||
];
|
||||
|
||||
public getComponent(): GenericConstructor<Component> {
|
||||
return getComponentByBrowseByType(this.browseByType, this.context, this.themeService.getThemeName());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,28 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for BrowseBySwitcherComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-browse-by-switcher',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedBrowseBySwitcherComponent extends ThemedComponent<BrowseBySwitcherComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'BrowseBySwitcherComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/browse-by/browse-by-switcher/browse-by-switcher.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./browse-by-switcher.component`);
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -1,14 +1,14 @@
|
||||
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { BROWSE_BY_COMPONENT_FACTORY } from '../browse-by-switcher/browse-by-decorator';
|
||||
import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ThemeService } from 'src/app/shared/theme-support/theme.service';
|
||||
import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model';
|
||||
import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type';
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-taxonomy-page',
|
||||
@@ -18,8 +18,19 @@ import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-bro
|
||||
/**
|
||||
* Component for browsing items by metadata in a hierarchical controlled vocabulary
|
||||
*/
|
||||
@rendersBrowseBy(BrowseByDataType.Hierarchy)
|
||||
export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* The optional context
|
||||
*/
|
||||
@Input() context: Context;
|
||||
|
||||
/**
|
||||
* The {@link BrowseByDataType} of this Component
|
||||
*/
|
||||
@Input() browseByType: BrowseByDataType;
|
||||
|
||||
/**
|
||||
* The {@link VocabularyOptions} object
|
||||
*/
|
||||
@@ -51,28 +62,27 @@ export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
|
||||
queryParams: any;
|
||||
|
||||
/**
|
||||
* Resolved browse-by component
|
||||
* Resolved browse-by definition
|
||||
*/
|
||||
browseByComponent: Observable<any>;
|
||||
browseDefinition$: Observable<BrowseDefinition>;
|
||||
|
||||
/**
|
||||
* Subscriptions to track
|
||||
*/
|
||||
browseByComponentSubs: Subscription[] = [];
|
||||
subs: Subscription[] = [];
|
||||
|
||||
public constructor( protected route: ActivatedRoute,
|
||||
protected themeService: ThemeService,
|
||||
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor<any>) {
|
||||
public constructor(
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.browseByComponent = this.route.data.pipe(
|
||||
this.browseDefinition$ = this.route.data.pipe(
|
||||
map((data: { browseDefinition: BrowseDefinition }) => {
|
||||
this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName());
|
||||
return data.browseDefinition;
|
||||
})
|
||||
);
|
||||
this.browseByComponentSubs.push(this.browseByComponent.subscribe((browseDefinition: HierarchicalBrowseDefinition) => {
|
||||
this.subs.push(this.browseDefinition$.subscribe((browseDefinition: HierarchicalBrowseDefinition) => {
|
||||
this.facetType = browseDefinition.facetType;
|
||||
this.vocabularyName = browseDefinition.vocabulary;
|
||||
this.vocabularyOptions = { name: this.vocabularyName, closed: true };
|
||||
@@ -113,6 +123,6 @@ export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.browseByComponentSubs.forEach((sub: Subscription) => sub.unsubscribe());
|
||||
this.subs.forEach((sub: Subscription) => sub.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
@@ -1,28 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
||||
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-browse-by-taxonomy-page',
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
styleUrls: []
|
||||
})
|
||||
/**
|
||||
* Themed wrapper for BrowseByTaxonomyPageComponent
|
||||
*/
|
||||
@rendersBrowseBy('hierarchy')
|
||||
export class ThemedBrowseByTaxonomyPageComponent extends ThemedComponent<BrowseByTaxonomyPageComponent>{
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'BrowseByTaxonomyPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./browse-by-taxonomy-page.component`);
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import {
|
||||
BrowseByMetadataPageComponent,
|
||||
browseParamsToOptions, getBrowseSearchOptions
|
||||
@@ -14,6 +13,8 @@ import { map } from 'rxjs/operators';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
||||
import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-title-page',
|
||||
@@ -23,7 +24,8 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
/**
|
||||
* Component for browsing items by title (dc.title)
|
||||
*/
|
||||
export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||
@rendersBrowseBy(BrowseByDataType.Title)
|
||||
export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent implements OnInit {
|
||||
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
protected browseService: BrowseService,
|
||||
@@ -57,8 +59,4 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||
this.updateStartsWithTextOptions();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,29 +0,0 @@
|
||||
import {Component} from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { BrowseByTitlePageComponent } from './browse-by-title-page.component';
|
||||
import {BrowseByDataType, rendersBrowseBy} from '../browse-by-switcher/browse-by-decorator';
|
||||
|
||||
/**
|
||||
* Themed wrapper for BrowseByTitlePageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-browse-by-title-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
|
||||
@rendersBrowseBy(BrowseByDataType.Title)
|
||||
export class ThemedBrowseByTitlePageComponent
|
||||
extends ThemedComponent<BrowseByTitlePageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'BrowseByTitlePageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/browse-by/browse-by-title-page/browse-by-title-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./browse-by-title-page.component`);
|
||||
}
|
||||
}
|
@@ -5,28 +5,22 @@ import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-
|
||||
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
|
||||
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
|
||||
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/browse-by-taxonomy-page.component';
|
||||
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
|
||||
import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
|
||||
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
|
||||
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
|
||||
import { ThemedBrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component';
|
||||
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||
import { FormModule } from '../shared/form/form.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
const DECLARATIONS = [
|
||||
BrowseBySwitcherComponent,
|
||||
];
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
BrowseByTitlePageComponent,
|
||||
BrowseByMetadataPageComponent,
|
||||
BrowseByDatePageComponent,
|
||||
BrowseByTaxonomyPageComponent,
|
||||
|
||||
ThemedBrowseByMetadataPageComponent,
|
||||
ThemedBrowseByDatePageComponent,
|
||||
ThemedBrowseByTitlePageComponent,
|
||||
ThemedBrowseByTaxonomyPageComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -39,12 +33,12 @@ const ENTRY_COMPONENTS = [
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
BrowseBySwitcherComponent,
|
||||
ThemedBrowseBySwitcherComponent,
|
||||
...DECLARATIONS,
|
||||
...ENTRY_COMPONENTS
|
||||
],
|
||||
exports: [
|
||||
BrowseBySwitcherComponent
|
||||
...DECLARATIONS,
|
||||
...ENTRY_COMPONENTS,
|
||||
]
|
||||
})
|
||||
export class BrowseByModule {
|
||||
|
@@ -105,7 +105,7 @@ export class BrowseService {
|
||||
})
|
||||
);
|
||||
if (options.fetchThumbnail ) {
|
||||
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
|
||||
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW);
|
||||
}
|
||||
return this.hrefOnlyDataService.findListByHref<BrowseEntry>(href$);
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export class BrowseService {
|
||||
}),
|
||||
);
|
||||
if (options.fetchThumbnail) {
|
||||
return this.hrefOnlyDataService.findListByHref<Item>(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW);
|
||||
return this.hrefOnlyDataService.findListByHref<Item>(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW);
|
||||
}
|
||||
return this.hrefOnlyDataService.findListByHref<Item>(href$);
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
import { CacheableObject } from '../cache/cacheable-object.model';
|
||||
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type';
|
||||
|
||||
/**
|
||||
* Base class for BrowseDefinition models
|
||||
@@ -12,5 +13,5 @@ export abstract class BrowseDefinition extends CacheableObject {
|
||||
/**
|
||||
* Get the render type of the BrowseDefinition model
|
||||
*/
|
||||
abstract getRenderType(): string;
|
||||
abstract getRenderType(): BrowseByDataType;
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import { FLAT_BROWSE_DEFINITION } from './flat-browse-definition.resource-type';
|
||||
import { ResourceType } from './resource-type';
|
||||
import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition';
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type';
|
||||
|
||||
/**
|
||||
* BrowseDefinition model for browses of type 'flatBrowse'
|
||||
@@ -30,7 +31,7 @@ export class FlatBrowseDefinition extends NonHierarchicalBrowseDefinition {
|
||||
items: HALLink;
|
||||
};
|
||||
|
||||
getRenderType(): string {
|
||||
getRenderType(): BrowseByDataType {
|
||||
return this.dataType;
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import { HIERARCHICAL_BROWSE_DEFINITION } from './hierarchical-browse-definition
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { ResourceType } from './resource-type';
|
||||
import { BrowseDefinition } from './browse-definition.model';
|
||||
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type';
|
||||
|
||||
/**
|
||||
* BrowseDefinition model for browses of type 'hierarchicalBrowse'
|
||||
@@ -39,7 +40,7 @@ export class HierarchicalBrowseDefinition extends BrowseDefinition {
|
||||
vocabulary: HALLink;
|
||||
};
|
||||
|
||||
getRenderType(): string {
|
||||
return 'hierarchy';
|
||||
getRenderType(): BrowseByDataType {
|
||||
return BrowseByDataType.Hierarchy;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { SortOption } from './sort-option.model';
|
||||
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator';
|
||||
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type';
|
||||
import { BrowseDefinition } from './browse-definition.model';
|
||||
|
||||
/**
|
||||
|
@@ -5,6 +5,7 @@ import { VALUE_LIST_BROWSE_DEFINITION } from './value-list-browse-definition.res
|
||||
import { ResourceType } from './resource-type';
|
||||
import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition';
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type';
|
||||
|
||||
/**
|
||||
* BrowseDefinition model for browses of type 'valueList'
|
||||
@@ -30,7 +31,7 @@ export class ValueListBrowseDefinition extends NonHierarchicalBrowseDefinition {
|
||||
entries: HALLink;
|
||||
};
|
||||
|
||||
getRenderType(): string {
|
||||
getRenderType(): BrowseByDataType {
|
||||
return this.dataType;
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { BrowseService } from '../core/browse/browse.service';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { buildPaginatedList } from '../core/data/paginated-list.model';
|
||||
import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator';
|
||||
import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-data-type';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { ThemeService } from '../shared/theme-support/theme.service';
|
||||
|
@@ -0,0 +1 @@
|
||||
<ng-template dsDynamicComponentLoader></ng-template>
|
@@ -0,0 +1,143 @@
|
||||
import { Component, ComponentRef, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { ThemeService } from '../theme-support/theme.service';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { hasValue, isNotEmpty } from '../empty.util';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { DynamicComponentLoaderDirective } from './dynamic-component-loader.directive';
|
||||
|
||||
/**
|
||||
* To create a new loader component you will need to:
|
||||
* <ul>
|
||||
* <li>Create a new LoaderComponent component extending this component</li>
|
||||
* <li>Point the templateUrl to this component's templateUrl</li>
|
||||
* <li>Add all the @Input()/@Output() names that the dynamically generated components should inherit from the loader to the inputNames/outputNames lists</li>
|
||||
* <li>Create a decorator file containing the new decorator function, a map containing all the collected {@link Component}s and a function to retrieve the {@link Component}</li>
|
||||
* <li>Call the function to retrieve the correct {@link Component} in getComponent()</li>
|
||||
* <li>Add all the @Input()s you had to used in getComponent() in the inputNamesDependentForComponent array</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-abstract-component-loader',
|
||||
templateUrl: './abstract-component-loader.component.html',
|
||||
})
|
||||
export abstract class AbstractComponentLoaderComponent<T> implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
/**
|
||||
* Directive to determine where the dynamic child component is located
|
||||
*/
|
||||
@ViewChild(DynamicComponentLoaderDirective, { static: true }) componentDirective: DynamicComponentLoaderDirective;
|
||||
|
||||
/**
|
||||
* The reference to the dynamic component
|
||||
*/
|
||||
protected compRef: ComponentRef<T>;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* The @Input() that are used to find the matching component using {@link getComponent}. When the value of
|
||||
* one of these @Input() change this loader needs to retrieve the best matching component again using the
|
||||
* {@link getComponent} method.
|
||||
*/
|
||||
protected inputNamesDependentForComponent: (keyof this & string)[] = [];
|
||||
|
||||
/**
|
||||
* The list of the @Input() names that should be passed down to the dynamically created components.
|
||||
*/
|
||||
protected inputNames: (keyof this & string)[] = [];
|
||||
|
||||
/**
|
||||
* The list of the @Output() names that should be passed down to the dynamically created components.
|
||||
*/
|
||||
protected outputNames: (keyof this & string)[] = [];
|
||||
|
||||
constructor(
|
||||
protected themeService: ThemeService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the dynamic child component
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.instantiateComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever the inputs change, update the inputs of the dynamic component
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (hasValue(this.compRef)) {
|
||||
if (this.inputNamesDependentForComponent.some((name: keyof this & string) => hasValue(changes[name]) && changes[name].previousValue !== changes[name].currentValue)) {
|
||||
// Recreate the component when the @Input()s used by getComponent() aren't up-to-date anymore
|
||||
this.destroyComponentInstance();
|
||||
this.instantiateComponent();
|
||||
} else {
|
||||
this.connectInputsAndOutputs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs
|
||||
.filter((subscription: Subscription) => hasValue(subscription))
|
||||
.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||
this.destroyComponentInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the component and connects the @Input() & @Output() from the ThemedComponent to its child Component.
|
||||
*/
|
||||
public instantiateComponent(): void {
|
||||
const component: GenericConstructor<T> = this.getComponent();
|
||||
|
||||
const viewContainerRef: ViewContainerRef = this.componentDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
this.compRef = viewContainerRef.createComponent(
|
||||
component, {
|
||||
index: 0,
|
||||
injector: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
this.connectInputsAndOutputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the themed component and calls it's `ngOnDestroy`
|
||||
*/
|
||||
public destroyComponentInstance(): void {
|
||||
if (hasValue(this.compRef)) {
|
||||
this.compRef.destroy();
|
||||
this.compRef = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the component depending on the item's entity type, metadata representation type and context
|
||||
*/
|
||||
public abstract getComponent(): GenericConstructor<T>;
|
||||
|
||||
/**
|
||||
* Connect the inputs and outputs of this component to the dynamic component,
|
||||
* to ensure they're in sync, the ngOnChanges method will automatically be called by setInput
|
||||
*/
|
||||
public connectInputsAndOutputs(): void {
|
||||
if (isNotEmpty(this.inputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
|
||||
this.inputNames.filter((name: string) => this[name] !== undefined).filter((name: string) => this[name] !== this.compRef.instance[name]).forEach((name: string) => {
|
||||
// Using setInput will automatically trigger the ngOnChanges
|
||||
this.compRef.setInput(name, this[name]);
|
||||
});
|
||||
}
|
||||
if (isNotEmpty(this.outputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
|
||||
this.outputNames.filter((name: string) => this[name] !== undefined).filter((name: string) => this[name] !== this.compRef.instance[name]).forEach((name: string) => {
|
||||
this.compRef.instance[name] = this[name];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
import { Directive, ViewContainerRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[dsAdvancedWorkflowActions]',
|
||||
})
|
||||
/**
|
||||
* Directive used as a hook to know where to inject the dynamic Advanced Claimed Task Actions component
|
||||
* Directive used as a hook to know where to inject the dynamic loaded component
|
||||
*/
|
||||
export class AdvancedWorkflowActionsDirective {
|
||||
@Directive({
|
||||
selector: '[dsDynamicComponentLoader]'
|
||||
})
|
||||
export class DynamicComponentLoaderDirective {
|
||||
|
||||
constructor(
|
||||
public viewContainerRef: ViewContainerRef,
|
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
Directive,
|
||||
Input,
|
||||
@@ -12,6 +11,7 @@ import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning';
|
||||
import { ContextHelpWrapperComponent } from './context-help-wrapper/context-help-wrapper.component';
|
||||
import { PlacementDir } from './context-help-wrapper/placement-dir.model';
|
||||
import { ContextHelpService } from './context-help.service';
|
||||
import { hasValue } from './empty.util';
|
||||
|
||||
export interface ContextHelpDirectiveInput {
|
||||
content: string;
|
||||
@@ -43,7 +43,6 @@ export class ContextHelpDirective implements OnChanges, OnDestroy {
|
||||
constructor(
|
||||
private templateRef: TemplateRef<any>,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private contextHelpService: ContextHelpService
|
||||
) {}
|
||||
|
||||
@@ -53,19 +52,21 @@ export class ContextHelpDirective implements OnChanges, OnDestroy {
|
||||
this.contextHelpService.add({id: this.dsContextHelp.id, isTooltipVisible: false});
|
||||
|
||||
if (this.wrapper === undefined) {
|
||||
const factory
|
||||
= this.componentFactoryResolver.resolveComponentFactory(ContextHelpWrapperComponent);
|
||||
this.wrapper = this.viewContainerRef.createComponent(factory);
|
||||
this.wrapper = this.viewContainerRef.createComponent(ContextHelpWrapperComponent);
|
||||
}
|
||||
this.wrapper.instance.templateRef = this.templateRef;
|
||||
this.wrapper.instance.content = this.dsContextHelp.content;
|
||||
this.wrapper.instance.id = this.dsContextHelp.id;
|
||||
this.wrapper.instance.tooltipPlacement = this.dsContextHelp.tooltipPlacement;
|
||||
this.wrapper.instance.iconPlacement = this.dsContextHelp.iconPlacement;
|
||||
this.wrapper.setInput('templateRef', this.templateRef);
|
||||
this.wrapper.setInput('content', this.dsContextHelp.content);
|
||||
this.wrapper.setInput('id', this.dsContextHelp.id);
|
||||
this.wrapper.setInput('tooltipPlacement', this.dsContextHelp.tooltipPlacement);
|
||||
this.wrapper.setInput('iconPlacement', this.dsContextHelp.iconPlacement);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.clearMostRecentId();
|
||||
if (hasValue(this.wrapper)) {
|
||||
this.wrapper.destroy();
|
||||
this.wrapper = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private clearMostRecentId(): void {
|
||||
|
@@ -29,6 +29,10 @@ import { RemoteDataBuildService } from '../../../../../core/cache/builders/remot
|
||||
import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||
import { followLink } from '../../../../utils/follow-link-config.model';
|
||||
import { RelationshipType } from '../../../../../core/shared/item-relationships/relationship-type.model';
|
||||
import { FindListOptions } from '../../../../../core/data/find-list-options.model';
|
||||
import { RequestParam } from '../../../../../core/cache/models/request-param.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup-relation-modal',
|
||||
@@ -173,6 +177,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
if (!!this.currentItemIsLeftItem$) {
|
||||
this.currentItemIsLeftItem$.subscribe((isLeft) => {
|
||||
this.isLeft = isLeft;
|
||||
this.label = this.relationshipType.leftwardType;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,6 +206,19 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
).pipe(
|
||||
getAllSucceededRemoteDataPayload()
|
||||
);
|
||||
} else {
|
||||
const findListOptions = Object.assign({}, new FindListOptions(), {
|
||||
elementsPerPage: 5,
|
||||
currentPage: 1,
|
||||
searchParams: [
|
||||
new RequestParam('entityType', this.relationshipOptions.relationshipType)
|
||||
]
|
||||
});
|
||||
this.externalSourcesRD$ = this.externalSourceService.searchBy('findByEntityType', findListOptions,
|
||||
true, true, followLink('entityTypes'))
|
||||
.pipe(getFirstSucceededRemoteDataPayload(), map((r: PaginatedList<ExternalSource>) => {
|
||||
return r.page;
|
||||
}));
|
||||
}
|
||||
|
||||
this.setTotals();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, ComponentFactoryResolver, ChangeDetectorRef } from '@angular/core';
|
||||
import { Component, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { ThemedComponent } from '../theme-support/themed.component';
|
||||
import { LoadingComponent } from './loading.component';
|
||||
import { ThemeService } from '../theme-support/theme.service';
|
||||
@@ -20,11 +20,10 @@ export class ThemedLoadingComponent extends ThemedComponent<LoadingComponent> {
|
||||
protected inAndOutputNames: (keyof LoadingComponent & keyof this)[] = ['message', 'showMessage', 'spinner'];
|
||||
|
||||
constructor(
|
||||
protected resolver: ComponentFactoryResolver,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
protected themeService: ThemeService
|
||||
) {
|
||||
super(resolver, cdr, themeService);
|
||||
super(cdr, themeService);
|
||||
}
|
||||
|
||||
protected getComponentName(): string {
|
||||
|
@@ -1 +0,0 @@
|
||||
<ng-container dsMetadataRepresentation></ng-container>
|
@@ -6,10 +6,11 @@ import {
|
||||
MetadataRepresentationType
|
||||
} from '../../core/shared/metadata-representation/metadata-representation.model';
|
||||
import { MetadataRepresentationLoaderComponent } from './metadata-representation-loader.component';
|
||||
import { MetadataRepresentationDirective } from './metadata-representation.directive';
|
||||
import { DynamicComponentLoaderDirective } from '../abstract-component-loader/dynamic-component-loader.directive';
|
||||
import { METADATA_REPRESENTATION_COMPONENT_FACTORY } from './metadata-representation.decorator';
|
||||
import { ThemeService } from '../theme-support/theme.service';
|
||||
import { PlainTextMetadataListElementComponent } from '../object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component';
|
||||
import { getMockThemeService } from '../mocks/theme-service.mock';
|
||||
|
||||
const testType = 'TestType';
|
||||
const testContext = Context.Search;
|
||||
@@ -36,12 +37,14 @@ describe('MetadataRepresentationLoaderComponent', () => {
|
||||
const themeName = 'test-theme';
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
themeService = jasmine.createSpyObj('themeService', {
|
||||
getThemeName: themeName,
|
||||
});
|
||||
themeService = getMockThemeService(themeName);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
declarations: [MetadataRepresentationLoaderComponent, PlainTextMetadataListElementComponent, MetadataRepresentationDirective],
|
||||
declarations: [
|
||||
MetadataRepresentationLoaderComponent,
|
||||
PlainTextMetadataListElementComponent,
|
||||
DynamicComponentLoaderDirective,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
providers: [
|
||||
{
|
||||
@@ -64,6 +67,7 @@ describe('MetadataRepresentationLoaderComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(MetadataRepresentationLoaderComponent);
|
||||
comp = fixture.componentInstance;
|
||||
spyOn(comp, 'getComponent').and.callThrough();
|
||||
|
||||
comp.mdRepresentation = new TestType();
|
||||
comp.context = testContext;
|
||||
@@ -71,8 +75,8 @@ describe('MetadataRepresentationLoaderComponent', () => {
|
||||
}));
|
||||
|
||||
describe('When the component is rendered', () => {
|
||||
it('should call the getMetadataRepresentationComponent function with the right entity type, representation type and context', () => {
|
||||
expect((comp as any).getMetadataRepresentationComponent).toHaveBeenCalledWith(testType, testRepresentationType, testContext, themeName);
|
||||
it('should call the getComponent function', () => {
|
||||
expect(comp.getComponent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentFactoryResolver, Inject, Input, OnInit, ViewChild, OnChanges, SimpleChanges, ComponentRef, ViewContainerRef, ComponentFactory } from '@angular/core';
|
||||
import { Component, Inject, Input } from '@angular/core';
|
||||
import {
|
||||
MetadataRepresentation,
|
||||
MetadataRepresentationType
|
||||
@@ -7,118 +7,44 @@ import { METADATA_REPRESENTATION_COMPONENT_FACTORY } from './metadata-representa
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component';
|
||||
import { MetadataRepresentationDirective } from './metadata-representation.directive';
|
||||
import { hasValue, isNotEmpty, hasNoValue } from '../empty.util';
|
||||
import { ThemeService } from '../theme-support/theme.service';
|
||||
import { AbstractComponentLoaderComponent } from '../abstract-component-loader/abstract-component-loader.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-representation-loader',
|
||||
templateUrl: './metadata-representation-loader.component.html'
|
||||
templateUrl: '../abstract-component-loader/abstract-component-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
|
||||
*/
|
||||
export class MetadataRepresentationLoaderComponent implements OnInit, OnChanges {
|
||||
export class MetadataRepresentationLoaderComponent extends AbstractComponentLoaderComponent<MetadataRepresentationListElementComponent> {
|
||||
|
||||
@Input() context: Context;
|
||||
|
||||
/**
|
||||
* The item or metadata to determine the component for
|
||||
*/
|
||||
private _mdRepresentation: MetadataRepresentation;
|
||||
get mdRepresentation(): MetadataRepresentation {
|
||||
return this._mdRepresentation;
|
||||
}
|
||||
@Input() set mdRepresentation(nextValue: MetadataRepresentation) {
|
||||
this._mdRepresentation = nextValue;
|
||||
if (hasValue(this.compRef?.instance)) {
|
||||
this.compRef.instance.mdRepresentation = nextValue;
|
||||
}
|
||||
}
|
||||
@Input() mdRepresentation: MetadataRepresentation;
|
||||
|
||||
/**
|
||||
* The optional context
|
||||
*/
|
||||
@Input() context: Context;
|
||||
protected inputNamesDependentForComponent: (keyof this & string)[] = [
|
||||
'context',
|
||||
'mdRepresentation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Directive to determine where the dynamic child component is located
|
||||
*/
|
||||
@ViewChild(MetadataRepresentationDirective, {static: true}) mdRepDirective: MetadataRepresentationDirective;
|
||||
|
||||
/**
|
||||
* The reference to the dynamic component
|
||||
*/
|
||||
protected compRef: ComponentRef<MetadataRepresentationListElementComponent>;
|
||||
|
||||
protected inAndOutputNames: (keyof this)[] = [
|
||||
protected inputNames: (keyof this & string)[] = [
|
||||
'context',
|
||||
'mdRepresentation',
|
||||
];
|
||||
|
||||
constructor(
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private themeService: ThemeService,
|
||||
protected themeService: ThemeService,
|
||||
@Inject(METADATA_REPRESENTATION_COMPONENT_FACTORY) private getMetadataRepresentationComponent: (entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context, theme: string) => GenericConstructor<any>,
|
||||
) {
|
||||
super(themeService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the dynamic child component
|
||||
*/
|
||||
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 componentFactory: ComponentFactory<MetadataRepresentationListElementComponent> = this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
|
||||
|
||||
const viewContainerRef: ViewContainerRef = this.mdRepDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
this.compRef = viewContainerRef.createComponent(componentFactory);
|
||||
|
||||
if (hasValue(changes)) {
|
||||
this.ngOnChanges(changes);
|
||||
} else {
|
||||
this.connectInputsAndOutputs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the component depending on the item's entity type, metadata representation type and context
|
||||
* @returns {string}
|
||||
*/
|
||||
private getComponent(): GenericConstructor<MetadataRepresentationListElementComponent> {
|
||||
public getComponent(): GenericConstructor<MetadataRepresentationListElementComponent> {
|
||||
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];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +0,0 @@
|
||||
import { Directive, ViewContainerRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[dsMetadataRepresentation]',
|
||||
})
|
||||
/**
|
||||
* Directive used as a hook to know where to inject the dynamic metadata representation component
|
||||
*/
|
||||
export class MetadataRepresentationDirective {
|
||||
constructor(public viewContainerRef: ViewContainerRef) { }
|
||||
}
|
@@ -1 +0,0 @@
|
||||
<ng-template dsClaimedTaskActions></ng-template>
|
@@ -1,7 +1,7 @@
|
||||
import { ClaimedTaskActionsLoaderComponent } from './claimed-task-actions-loader.component';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive';
|
||||
import { DynamicComponentLoaderDirective } from '../../../abstract-component-loader/dynamic-component-loader.directive';
|
||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ClaimedTaskActionsEditMetadataComponent } from '../edit-metadata/claimed-task-actions-edit-metadata.component';
|
||||
@@ -17,6 +17,8 @@ import { getMockSearchService } from '../../../mocks/search-service.mock';
|
||||
import { getMockRequestService } from '../../../mocks/request.service.mock';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
import { ThemeService } from 'src/app/shared/theme-support/theme.service';
|
||||
import { getMockThemeService } from '../../../mocks/theme-service.mock';
|
||||
|
||||
const searchService = getMockSearchService();
|
||||
|
||||
@@ -25,6 +27,7 @@ const requestService = getMockRequestService();
|
||||
describe('ClaimedTaskActionsLoaderComponent', () => {
|
||||
let comp: ClaimedTaskActionsLoaderComponent;
|
||||
let fixture: ComponentFixture<ClaimedTaskActionsLoaderComponent>;
|
||||
let themeService: ThemeService;
|
||||
|
||||
const option = 'test_option';
|
||||
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
|
||||
@@ -61,9 +64,15 @@ describe('ClaimedTaskActionsLoaderComponent', () => {
|
||||
const workflowitem = Object.assign(new WorkflowItem(), { id: '333' });
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
themeService = getMockThemeService('dspace');
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ClaimedTaskActionsLoaderComponent, ClaimedTaskActionsEditMetadataComponent, ClaimedTaskActionsDirective],
|
||||
declarations: [
|
||||
ClaimedTaskActionsLoaderComponent,
|
||||
ClaimedTaskActionsEditMetadataComponent,
|
||||
DynamicComponentLoaderDirective,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
providers: [
|
||||
{ provide: ClaimedTaskDataService, useValue: {} },
|
||||
@@ -72,7 +81,8 @@ describe('ClaimedTaskActionsLoaderComponent', () => {
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: SearchService, useValue: searchService },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: PoolTaskDataService, useValue: {} }
|
||||
{ provide: PoolTaskDataService, useValue: {} },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
]
|
||||
}).overrideComponent(ClaimedTaskActionsLoaderComponent, {
|
||||
set: {
|
||||
@@ -89,14 +99,14 @@ describe('ClaimedTaskActionsLoaderComponent', () => {
|
||||
comp.object = object;
|
||||
comp.option = option;
|
||||
comp.workflowitem = workflowitem;
|
||||
spyOn(comp, 'getComponentByWorkflowTaskOption').and.returnValue(ClaimedTaskActionsEditMetadataComponent);
|
||||
spyOn(comp, 'getComponent').and.returnValue(ClaimedTaskActionsEditMetadataComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
describe('When the component is rendered', () => {
|
||||
it('should call the getComponentByWorkflowTaskOption function with the right option', () => {
|
||||
expect(comp.getComponentByWorkflowTaskOption).toHaveBeenCalledWith(option);
|
||||
it('should call the getComponent function', () => {
|
||||
expect(comp.getComponent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,33 +1,22 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
ComponentRef,
|
||||
} from '@angular/core';
|
||||
import { Component, EventEmitter, Input, Output, } from '@angular/core';
|
||||
import { getComponentByWorkflowTaskOption } from './claimed-task-actions-decorator';
|
||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||
import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive';
|
||||
import { hasValue, isNotEmpty, hasNoValue } from '../../../empty.util';
|
||||
import { MyDSpaceActionsResult } from '../../mydspace-actions';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
|
||||
import { AbstractComponentLoaderComponent } from '../../../abstract-component-loader/abstract-component-loader.component';
|
||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-claimed-task-actions-loader',
|
||||
templateUrl: './claimed-task-actions-loader.component.html'
|
||||
templateUrl: '../../../abstract-component-loader/abstract-component-loader.component.html',
|
||||
})
|
||||
/**
|
||||
* Component for loading a ClaimedTaskAction component depending on the "option" input
|
||||
* Passes on the ClaimedTask to the component and subscribes to the processCompleted output
|
||||
*/
|
||||
export class ClaimedTaskActionsLoaderComponent implements OnInit, OnChanges {
|
||||
export class ClaimedTaskActionsLoaderComponent extends AbstractComponentLoaderComponent<ClaimedTaskActionsAbstractComponent> {
|
||||
/**
|
||||
* The item object that belonging to the ClaimedTask object
|
||||
*/
|
||||
@@ -54,85 +43,22 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnChanges {
|
||||
*/
|
||||
@Output() processCompleted = new EventEmitter<MyDSpaceActionsResult>();
|
||||
|
||||
/**
|
||||
* Directive to determine where the dynamic child component is located
|
||||
*/
|
||||
@ViewChild(ClaimedTaskActionsDirective, {static: true}) claimedTaskActionsDirective: ClaimedTaskActionsDirective;
|
||||
|
||||
/**
|
||||
* The reference to the dynamic component
|
||||
*/
|
||||
protected compRef: ComponentRef<Component>;
|
||||
|
||||
/**
|
||||
* The list of input and output names for the dynamic component
|
||||
*/
|
||||
protected inAndOutputNames: (keyof ClaimedTaskActionsAbstractComponent & keyof this)[] = [
|
||||
protected inputNames: (keyof this & string)[] = [
|
||||
'item',
|
||||
'object',
|
||||
'option',
|
||||
'workflowitem',
|
||||
];
|
||||
|
||||
protected outputNames: (keyof this & string)[] = [
|
||||
'processCompleted',
|
||||
];
|
||||
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
public getComponent(): GenericConstructor<ClaimedTaskActionsAbstractComponent> {
|
||||
return getComponentByWorkflowTaskOption(this.option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch, create and initialize the relevant component
|
||||
*/
|
||||
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);
|
||||
if (hasValue(comp)) {
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp);
|
||||
|
||||
const viewContainerRef = this.claimedTaskActionsDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
this.compRef = viewContainerRef.createComponent(componentFactory);
|
||||
|
||||
if (hasValue(changes)) {
|
||||
this.ngOnChanges(changes);
|
||||
} else {
|
||||
this.connectInputsAndOutputs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getComponentByWorkflowTaskOption(option: string) {
|
||||
return getComponentByWorkflowTaskOption(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +0,0 @@
|
||||
import { Directive, ViewContainerRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[dsClaimedTaskActions]',
|
||||
})
|
||||
/**
|
||||
* Directive used as a hook to know where to inject the dynamic Claimed Task Actions component
|
||||
*/
|
||||
export class ClaimedTaskActionsDirective {
|
||||
constructor(public viewContainerRef: ViewContainerRef) { }
|
||||
}
|
@@ -1 +0,0 @@
|
||||
<ng-template dsListableObject></ng-template>
|
@@ -8,7 +8,7 @@ import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import {
|
||||
ItemListElementComponent
|
||||
} from '../../../object-list/item-list-element/item-types/item/item-list-element.component';
|
||||
import { ListableObjectDirective } from './listable-object.directive';
|
||||
import { DynamicComponentLoaderDirective } from '../../../abstract-component-loader/dynamic-component-loader.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
@@ -36,7 +36,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ListableObjectComponentLoaderComponent, ItemListElementComponent, ListableObjectDirective],
|
||||
declarations: [ListableObjectComponentLoaderComponent, ItemListElementComponent, DynamicComponentLoaderDirective],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
providers: [
|
||||
provideMockStore({}),
|
||||
@@ -65,7 +65,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
||||
|
||||
describe('When the component is rendered', () => {
|
||||
it('should call the getListableObjectComponent function with the right types, view mode and context', () => {
|
||||
expect(comp.getComponent).toHaveBeenCalledWith([testType], testViewMode, testContext);
|
||||
expect(comp.getComponent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should connectInputsAndOutputs of loaded component', () => {
|
||||
@@ -78,29 +78,29 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
||||
let reloadedObject: any;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn((comp as any), 'instantiateComponent').and.returnValue(null);
|
||||
spyOn((comp as any).contentChange, 'emit').and.returnValue(null);
|
||||
spyOn(comp, 'instantiateComponent').and.returnValue(null);
|
||||
spyOn(comp.contentChange, 'emit').and.returnValue(null);
|
||||
|
||||
listableComponent = fixture.debugElement.query(By.css('ds-item-list-element')).componentInstance;
|
||||
reloadedObject = 'object';
|
||||
});
|
||||
|
||||
it('should re-instantiate the listable component', fakeAsync(() => {
|
||||
expect((comp as any).instantiateComponent).not.toHaveBeenCalled();
|
||||
expect(comp.instantiateComponent).not.toHaveBeenCalled();
|
||||
|
||||
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
||||
listableComponent.reloadedObject.emit(reloadedObject);
|
||||
tick(200);
|
||||
|
||||
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject, undefined);
|
||||
expect(comp.instantiateComponent).toHaveBeenCalledWith();
|
||||
}));
|
||||
|
||||
it('should re-emit it as a contentChange', fakeAsync(() => {
|
||||
expect((comp as any).contentChange.emit).not.toHaveBeenCalled();
|
||||
expect(comp.contentChange.emit).not.toHaveBeenCalled();
|
||||
|
||||
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
||||
listableComponent.reloadedObject.emit(reloadedObject);
|
||||
tick(200);
|
||||
|
||||
expect((comp as any).contentChange.emit).toHaveBeenCalledWith(reloadedObject);
|
||||
expect(comp.contentChange.emit).toHaveBeenCalledWith(reloadedObject);
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -1,40 +1,25 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
|
||||
import { Subscription, combineLatest, of as observableOf, Observable } from 'rxjs';
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
import { ListableObject } from '../listable-object.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { Context } from 'src/app/core/shared/context.model';
|
||||
import { getListableObjectComponent } from './listable-object.decorator';
|
||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||
import { ListableObjectDirective } from './listable-object.directive';
|
||||
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
||||
import { hasValue, isNotEmpty, hasNoValue } from '../../../empty.util';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { ThemeService } from '../../../theme-support/theme.service';
|
||||
import { AbstractComponentLoaderComponent } from '../../../abstract-component-loader/abstract-component-loader.component';
|
||||
import { ThemeService } from 'src/app/shared/theme-support/theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-listable-object-component-loader',
|
||||
styleUrls: ['./listable-object-component-loader.component.scss'],
|
||||
templateUrl: './listable-object-component-loader.component.html'
|
||||
templateUrl: '../../../abstract-component-loader/abstract-component-loader.component.html',
|
||||
})
|
||||
/**
|
||||
* Component for determining what component to use depending on the item's entity type (dspace.entity.type)
|
||||
*/
|
||||
export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges, OnDestroy {
|
||||
export class ListableObjectComponentLoaderComponent extends AbstractComponentLoaderComponent<Component> {
|
||||
|
||||
/**
|
||||
* The item or metadata to determine the component for
|
||||
*/
|
||||
@@ -73,115 +58,60 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
|
||||
/**
|
||||
* Whether to show the thumbnail preview
|
||||
*/
|
||||
@Input() showThumbnails;
|
||||
@Input() showThumbnails: boolean;
|
||||
|
||||
/**
|
||||
* The value to display for this element
|
||||
*/
|
||||
@Input() value: string;
|
||||
|
||||
/**
|
||||
* Directive hook used to place the dynamic child component
|
||||
*/
|
||||
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
|
||||
|
||||
/**
|
||||
* Emit when the listable object has been reloaded.
|
||||
*/
|
||||
@Output() contentChange = new EventEmitter<ListableObject>();
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* The reference to the dynamic component
|
||||
*/
|
||||
protected compRef: ComponentRef<Component>;
|
||||
protected inputNamesDependentForComponent: (keyof this & string)[] = [
|
||||
'object',
|
||||
'viewMode',
|
||||
'context',
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of input and output names for the dynamic component
|
||||
*/
|
||||
protected inAndOutputNames: string[] = [
|
||||
protected inputNames: (keyof this & string)[] = [
|
||||
'object',
|
||||
'index',
|
||||
'context',
|
||||
'linkType',
|
||||
'listID',
|
||||
'showLabel',
|
||||
'showThumbnails',
|
||||
'context',
|
||||
'viewMode',
|
||||
'value',
|
||||
'hideBadges',
|
||||
];
|
||||
|
||||
protected outputNames: (keyof this & string)[] = [
|
||||
'contentChange',
|
||||
];
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef, private themeService: ThemeService) {
|
||||
constructor(
|
||||
protected themeService: ThemeService,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
) {
|
||||
super(themeService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the dynamic child component
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.instantiateComponent(this.object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs
|
||||
.filter((subscription) => hasValue(subscription))
|
||||
.forEach((subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
private instantiateComponent(object: ListableObject, changes?: SimpleChanges): void {
|
||||
|
||||
const component = this.getComponent(object.getRenderTypes(), this.viewMode, this.context);
|
||||
|
||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
this.compRef = viewContainerRef.createComponent(
|
||||
component, {
|
||||
index: 0,
|
||||
injector: undefined
|
||||
}
|
||||
);
|
||||
|
||||
if (hasValue(changes)) {
|
||||
this.ngOnChanges(changes);
|
||||
} else {
|
||||
this.connectInputsAndOutputs();
|
||||
}
|
||||
|
||||
public instantiateComponent(): void {
|
||||
super.instantiateComponent();
|
||||
if ((this.compRef.instance as any).reloadedObject) {
|
||||
combineLatest([
|
||||
observableOf(changes),
|
||||
(this.compRef.instance as any).reloadedObject.pipe(take(1)) as Observable<DSpaceObject>,
|
||||
]).subscribe(([simpleChanges, reloadedObject]: [SimpleChanges, DSpaceObject]) => {
|
||||
(this.compRef.instance as any).reloadedObject.pipe(
|
||||
take(1),
|
||||
).subscribe((reloadedObject: DSpaceObject) => {
|
||||
if (reloadedObject) {
|
||||
this.compRef.destroy();
|
||||
this.destroyComponentInstance();
|
||||
this.object = reloadedObject;
|
||||
this.instantiateComponent(reloadedObject, simpleChanges);
|
||||
this.instantiateComponent();
|
||||
this.cdr.detectChanges();
|
||||
this.contentChange.emit(reloadedObject);
|
||||
}
|
||||
@@ -189,26 +119,8 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the component depending on the item's entity type, view mode and context
|
||||
* @returns {GenericConstructor<Component>}
|
||||
*/
|
||||
getComponent(renderTypes: (string | GenericConstructor<ListableObject>)[],
|
||||
viewMode: ViewMode,
|
||||
context: Context): GenericConstructor<Component> {
|
||||
return getListableObjectComponent(renderTypes, viewMode, 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];
|
||||
});
|
||||
}
|
||||
public getComponent(): GenericConstructor<Component> {
|
||||
return getListableObjectComponent(this.object.getRenderTypes(), this.viewMode, this.context, this.themeService.getThemeName());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,11 +0,0 @@
|
||||
import { Directive, ViewContainerRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[dsListableObject]',
|
||||
})
|
||||
/**
|
||||
* Directive used as a hook to know where to inject the dynamic listable object component
|
||||
*/
|
||||
export class ListableObjectDirective {
|
||||
constructor(public viewContainerRef: ViewContainerRef) { }
|
||||
}
|
@@ -6,7 +6,6 @@ import {
|
||||
ComponentRef,
|
||||
SimpleChanges,
|
||||
OnDestroy,
|
||||
ComponentFactoryResolver,
|
||||
ChangeDetectorRef,
|
||||
OnChanges,
|
||||
HostBinding,
|
||||
@@ -47,7 +46,6 @@ export abstract class ThemedComponent<T> implements AfterViewInit, OnDestroy, On
|
||||
@HostBinding('attr.data-used-theme') usedTheme: string;
|
||||
|
||||
constructor(
|
||||
protected resolver: ComponentFactoryResolver,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
protected themeService: ThemeService,
|
||||
) {
|
||||
@@ -118,8 +116,9 @@ export abstract class ThemedComponent<T> implements AfterViewInit, OnDestroy, On
|
||||
|
||||
this.lazyLoadSub = this.lazyLoadObs.subscribe(([simpleChanges, constructor]: [SimpleChanges, GenericConstructor<T>]) => {
|
||||
this.destroyComponentInstance();
|
||||
const factory = this.resolver.resolveComponentFactory(constructor);
|
||||
this.compRef = this.vcr.createComponent(factory, undefined, undefined, [this.themedElementContent.nativeElement.childNodes]);
|
||||
this.compRef = this.vcr.createComponent(constructor, {
|
||||
projectableNodes: [this.themedElementContent.nativeElement.childNodes],
|
||||
});
|
||||
if (hasValue(simpleChanges)) {
|
||||
this.ngOnChanges(simpleChanges);
|
||||
} else {
|
||||
|
@@ -1 +0,0 @@
|
||||
<ng-template dsAdvancedWorkflowActions></ng-template>
|
@@ -3,12 +3,14 @@ import { AdvancedWorkflowActionsLoaderComponent } from './advanced-workflow-acti
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { AdvancedWorkflowActionsDirective } from './advanced-workflow-actions.directive';
|
||||
import { DynamicComponentLoaderDirective } from '../../../shared/abstract-component-loader/dynamic-component-loader.directive';
|
||||
import {
|
||||
rendersAdvancedWorkflowTaskOption
|
||||
} from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { PAGE_NOT_FOUND_PATH } from '../../../app-routing-paths';
|
||||
import { ThemeService } from 'src/app/shared/theme-support/theme.service';
|
||||
import { getMockThemeService } from 'src/app/shared/mocks/theme-service.mock';
|
||||
|
||||
const ADVANCED_WORKFLOW_ACTION_TEST = 'testaction';
|
||||
|
||||
@@ -17,17 +19,20 @@ describe('AdvancedWorkflowActionsLoaderComponent', () => {
|
||||
let fixture: ComponentFixture<AdvancedWorkflowActionsLoaderComponent>;
|
||||
|
||||
let router: RouterStub;
|
||||
let themeService: ThemeService;
|
||||
|
||||
beforeEach(async () => {
|
||||
router = new RouterStub();
|
||||
themeService = getMockThemeService();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AdvancedWorkflowActionsDirective,
|
||||
DynamicComponentLoaderDirective,
|
||||
AdvancedWorkflowActionsLoaderComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
],
|
||||
}).overrideComponent(AdvancedWorkflowActionsLoaderComponent, {
|
||||
set: {
|
||||
@@ -50,24 +55,24 @@ describe('AdvancedWorkflowActionsLoaderComponent', () => {
|
||||
|
||||
describe('When the component is rendered', () => {
|
||||
it('should display the AdvancedWorkflowActionTestComponent when the type has been defined in a rendersAdvancedWorkflowTaskOption', () => {
|
||||
spyOn(component, 'getComponentByWorkflowTaskOption').and.returnValue(AdvancedWorkflowActionTestComponent);
|
||||
spyOn(component, 'getComponent').and.returnValue(AdvancedWorkflowActionTestComponent);
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getComponentByWorkflowTaskOption).toHaveBeenCalledWith(ADVANCED_WORKFLOW_ACTION_TEST);
|
||||
expect(component.getComponent).toHaveBeenCalled();
|
||||
expect(fixture.debugElement.query(By.css('#AdvancedWorkflowActionsLoaderComponent'))).not.toBeNull();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to page not found when the type has not been defined in a rendersAdvancedWorkflowTaskOption', () => {
|
||||
spyOn(component, 'getComponentByWorkflowTaskOption').and.returnValue(undefined);
|
||||
spyOn(component, 'getComponent').and.returnValue(undefined);
|
||||
component.type = 'nonexistingaction';
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.getComponentByWorkflowTaskOption).toHaveBeenCalledWith('nonexistingaction');
|
||||
expect(component.getComponent).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]);
|
||||
});
|
||||
});
|
||||
|
@@ -1,21 +1,22 @@
|
||||
import { Component, Input, ViewChild, ComponentFactoryResolver, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import {
|
||||
getAdvancedComponentByWorkflowTaskOption
|
||||
} from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator';
|
||||
import { AdvancedWorkflowActionsDirective } from './advanced-workflow-actions.directive';
|
||||
import { Router } from '@angular/router';
|
||||
import { PAGE_NOT_FOUND_PATH } from '../../../app-routing-paths';
|
||||
import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
import { AbstractComponentLoaderComponent } from 'src/app/shared/abstract-component-loader/abstract-component-loader.component';
|
||||
import { ThemeService } from '../../../shared/theme-support/theme.service';
|
||||
|
||||
/**
|
||||
* Component for loading a {@link AdvancedWorkflowActionComponent} depending on the "{@link type}" input
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-advanced-workflow-actions-loader',
|
||||
templateUrl: './advanced-workflow-actions-loader.component.html',
|
||||
styleUrls: ['./advanced-workflow-actions-loader.component.scss'],
|
||||
templateUrl: '../../../shared/abstract-component-loader/abstract-component-loader.component.html',
|
||||
})
|
||||
export class AdvancedWorkflowActionsLoaderComponent implements OnInit {
|
||||
export class AdvancedWorkflowActionsLoaderComponent extends AbstractComponentLoaderComponent<Component> implements OnInit {
|
||||
|
||||
/**
|
||||
* The name of the type to render
|
||||
@@ -23,35 +24,28 @@ export class AdvancedWorkflowActionsLoaderComponent implements OnInit {
|
||||
*/
|
||||
@Input() type: string;
|
||||
|
||||
/**
|
||||
* Directive to determine where the dynamic child component is located
|
||||
*/
|
||||
@ViewChild(AdvancedWorkflowActionsDirective, { static: true }) claimedTaskActionsDirective: AdvancedWorkflowActionsDirective;
|
||||
protected inputNames: (keyof this & string)[] = [
|
||||
...this.inputNames,
|
||||
'type',
|
||||
];
|
||||
|
||||
constructor(
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
protected themeService: ThemeService,
|
||||
private router: Router,
|
||||
) {
|
||||
super(themeService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch, create and initialize the relevant component
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const comp = this.getComponentByWorkflowTaskOption(this.type);
|
||||
if (hasValue(comp)) {
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp);
|
||||
|
||||
const viewContainerRef = this.claimedTaskActionsDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
viewContainerRef.createComponent(componentFactory);
|
||||
if (hasValue(this.getComponent())) {
|
||||
super.ngOnInit();
|
||||
} else {
|
||||
void this.router.navigate([PAGE_NOT_FOUND_PATH]);
|
||||
}
|
||||
}
|
||||
|
||||
getComponentByWorkflowTaskOption(type: string): any {
|
||||
return getAdvancedComponentByWorkflowTaskOption(type);
|
||||
public getComponent(): GenericConstructor<Component> {
|
||||
return getAdvancedComponentByWorkflowTaskOption(this.type);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -23,9 +23,6 @@ import {
|
||||
import {
|
||||
AdvancedWorkflowActionPageComponent
|
||||
} from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component';
|
||||
import {
|
||||
AdvancedWorkflowActionsDirective
|
||||
} from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive';
|
||||
import { AccessControlModule } from '../access-control/access-control.module';
|
||||
import {
|
||||
ReviewersListComponent
|
||||
@@ -54,7 +51,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
AdvancedWorkflowActionRatingComponent,
|
||||
AdvancedWorkflowActionSelectReviewerComponent,
|
||||
AdvancedWorkflowActionPageComponent,
|
||||
AdvancedWorkflowActionsDirective,
|
||||
ReviewersListComponent,
|
||||
]
|
||||
})
|
||||
|
@@ -2766,6 +2766,8 @@
|
||||
|
||||
"journalissue.page.titleprefix": "Journal Issue: ",
|
||||
|
||||
"journalissue.search.results.head": "Journal Issue Search Results",
|
||||
|
||||
"journalvolume.listelement.badge": "Journal Volume",
|
||||
|
||||
"journalvolume.page.description": "Description",
|
||||
@@ -2778,6 +2780,8 @@
|
||||
|
||||
"journalvolume.page.volume": "Volume",
|
||||
|
||||
"journalvolume.search.results.head": "Journal Volume Search Results",
|
||||
|
||||
"iiifsearchable.listelement.badge": "Document Media",
|
||||
|
||||
"iiifsearchable.page.titleprefix": "Document: ",
|
||||
@@ -3684,10 +3688,14 @@
|
||||
|
||||
"relationships.isIssueOf": "Journal Issues",
|
||||
|
||||
"relationships.isIssueOf.JournalIssue": "Journal Issue",
|
||||
|
||||
"relationships.isJournalIssueOf": "Journal Issue",
|
||||
|
||||
"relationships.isJournalOf": "Journals",
|
||||
|
||||
"relationships.isJournalVolumeOf": "Journal Volume",
|
||||
|
||||
"relationships.isOrgUnitOf": "Organizational Units",
|
||||
|
||||
"relationships.isPersonOf": "Authors",
|
||||
@@ -3704,6 +3712,8 @@
|
||||
|
||||
"relationships.isVolumeOf": "Journal Volumes",
|
||||
|
||||
"relationships.isVolumeOf.JournalVolume": "Journal Volume",
|
||||
|
||||
"relationships.isContributorOf": "Contributors",
|
||||
|
||||
"relationships.isContributorOf.OrgUnit": "Contributor (Organizational Unit)",
|
||||
@@ -4434,6 +4444,16 @@
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.arxiv": "arXiv ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidWorks": "ORCID ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.crossref": "CrossRef ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.scopus": "Scopus ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.openaireFunding": "Funding OpenAIRE ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournalIssn": "Sherpa Journals by ISSN ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingAgencyOfPublication": "Search for Funding Agencies",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingOfPublication": "Search for Funding",
|
||||
@@ -4460,6 +4480,8 @@
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.title.Journal": "Journal",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.title.isJournalIssueOfPublication": "Journal Issues",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.title.JournalIssue": "Journal Issues",
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { BrowseByDatePageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-date-page/browse-by-date-page.component';
|
||||
import { BrowseByDataType } from '../../../../../app/browse-by/browse-by-switcher/browse-by-data-type';
|
||||
import { rendersBrowseBy } from '../../../../../app/browse-by/browse-by-switcher/browse-by-decorator';
|
||||
import { Context } from '../../../../../app/core/shared/context.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-date-page',
|
||||
@@ -8,10 +11,6 @@ import { BrowseByDatePageComponent as BaseComponent } from '../../../../../app/b
|
||||
// templateUrl: './browse-by-date-page.component.html'
|
||||
templateUrl: '../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* Component for determining what Browse-By component to use depending on the metadata (browse ID) provided
|
||||
*/
|
||||
|
||||
@rendersBrowseBy(BrowseByDataType.Date, Context.Any, 'custom')
|
||||
export class BrowseByDatePageComponent extends BaseComponent {
|
||||
}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { BrowseByMetadataPageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { BrowseByDataType } from '../../../../../app/browse-by/browse-by-switcher/browse-by-data-type';
|
||||
import { rendersBrowseBy } from '../../../../../app/browse-by/browse-by-switcher/browse-by-decorator';
|
||||
import { Context } from '../../../../../app/core/shared/context.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-metadata-page',
|
||||
@@ -8,10 +11,6 @@ import { BrowseByMetadataPageComponent as BaseComponent } from '../../../../../a
|
||||
// templateUrl: './browse-by-metadata-page.component.html'
|
||||
templateUrl: '../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* Component for determining what Browse-By component to use depending on the metadata (browse ID) provided
|
||||
*/
|
||||
|
||||
@rendersBrowseBy(BrowseByDataType.Metadata, Context.Any, 'custom')
|
||||
export class BrowseByMetadataPageComponent extends BaseComponent {
|
||||
}
|
||||
|
@@ -1,15 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { BrowseBySwitcherComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-switcher/browse-by-switcher.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-switcher',
|
||||
// styleUrls: ['./browse-by-switcher.component.scss'],
|
||||
// templateUrl: './browse-by-switcher.component.html'
|
||||
templateUrl: '../../../../../app/browse-by/browse-by-switcher/browse-by-switcher.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* Component for determining what Browse-By component to use depending on the metadata (browse ID) provided
|
||||
*/
|
||||
export class BrowseBySwitcherComponent extends BaseComponent {}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { BrowseByTaxonomyPageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component';
|
||||
import { BrowseByDataType } from '../../../../../app/browse-by/browse-by-switcher/browse-by-data-type';
|
||||
import { rendersBrowseBy } from '../../../../../app/browse-by/browse-by-switcher/browse-by-decorator';
|
||||
import { Context } from '../../../../../app/core/shared/context.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-taxonomy-page',
|
||||
@@ -8,8 +11,6 @@ import { BrowseByTaxonomyPageComponent as BaseComponent } from '../../../../../a
|
||||
// styleUrls: ['./browse-by-taxonomy-page.component.scss'],
|
||||
styleUrls: ['../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss'],
|
||||
})
|
||||
/**
|
||||
* Component for browsing items by metadata in a hierarchical controlled vocabulary
|
||||
*/
|
||||
@rendersBrowseBy(BrowseByDataType.Hierarchy, Context.Any, 'custom')
|
||||
export class BrowseByTaxonomyPageComponent extends BaseComponent {
|
||||
}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { BrowseByTitlePageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-title-page/browse-by-title-page.component';
|
||||
import { BrowseByDataType } from '../../../../../app/browse-by/browse-by-switcher/browse-by-data-type';
|
||||
import { rendersBrowseBy } from '../../../../../app/browse-by/browse-by-switcher/browse-by-decorator';
|
||||
import { Context } from '../../../../../app/core/shared/context.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-title-page',
|
||||
@@ -8,10 +11,6 @@ import { BrowseByTitlePageComponent as BaseComponent } from '../../../../../app/
|
||||
// templateUrl: './browse-by-title-page.component.html'
|
||||
templateUrl: '../../../../../app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* Component for determining what Browse-By component to use depending on the metadata (browse ID) provided
|
||||
*/
|
||||
|
||||
@rendersBrowseBy(BrowseByDataType.Title, Context.Any, 'custom')
|
||||
export class BrowseByTitlePageComponent extends BaseComponent {
|
||||
}
|
||||
|
@@ -46,7 +46,6 @@ import { RootModule } from '../../app/root.module';
|
||||
import { FileSectionComponent } from './app/item-page/simple/field-components/file-section/file-section.component';
|
||||
import { HomePageComponent } from './app/home-page/home-page.component';
|
||||
import { RootComponent } from './app/root/root.component';
|
||||
import { BrowseBySwitcherComponent } from './app/browse-by/browse-by-switcher/browse-by-switcher.component';
|
||||
import { CommunityListPageComponent } from './app/community-list-page/community-list-page.component';
|
||||
import { SearchPageComponent } from './app/search-page/search-page.component';
|
||||
import { ConfigurationSearchPageComponent } from './app/search-page/configuration-search-page.component';
|
||||
@@ -165,7 +164,6 @@ const DECLARATIONS = [
|
||||
FileSectionComponent,
|
||||
HomePageComponent,
|
||||
RootComponent,
|
||||
BrowseBySwitcherComponent,
|
||||
CommunityListPageComponent,
|
||||
SearchPageComponent,
|
||||
ConfigurationSearchPageComponent,
|
||||
|
Reference in New Issue
Block a user