Merge remote-tracking branch 'upstream/main' into use-applied-filter-to-display-label-on-search_contribute-main

# Conflicts:
#	src/app/core/shared/search/search-filter.service.ts
#	src/app/shared/search/search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component.ts
#	src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts
#	src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts
#	src/app/shared/search/search-filters/search-filter/search-filter.component.ts
#	src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts
#	src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts
#	src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
#	src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
#	src/app/shared/search/search-filters/search-filters.component.html
This commit is contained in:
Alexandre Vryghem
2024-02-16 20:20:16 +01:00
316 changed files with 9122 additions and 2037 deletions

View File

@@ -33,6 +33,8 @@ jobs:
#CHROME_VERSION: "90.0.4430.212-1"
# Bump Node heap size (OOM in CI after upgrading to Angular 15)
NODE_OPTIONS: '--max-old-space-size=4096'
# Project name to use when running docker-compose prior to e2e tests
COMPOSE_PROJECT_NAME: 'ci'
strategy:
# Create a matrix of Node versions to test against (in parallel)
matrix:

View File

@@ -386,3 +386,9 @@ vocabularies:
comcolSelectionSort:
sortField: 'dc.title'
sortDirection: 'ASC'
# Example of fallback collection for suggestions import
# suggestion:
# - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af
# source: "openaire"

View File

@@ -5,9 +5,9 @@ describe('Browse By Author', () => {
cy.visit('/browse/author');
// Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata-page').should('be.visible');
cy.get('ds-browse-by-metadata').should('be.visible');
// Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata-page');
testA11y('ds-browse-by-metadata');
});
});

View File

@@ -5,9 +5,9 @@ describe('Browse By Date Issued', () => {
cy.visit('/browse/dateissued');
// Wait for <ds-browse-by-date-page> to be visible
cy.get('ds-browse-by-date-page').should('be.visible');
cy.get('ds-browse-by-date').should('be.visible');
// Analyze <ds-browse-by-date-page> for accessibility
testA11y('ds-browse-by-date-page');
testA11y('ds-browse-by-date');
});
});

View File

@@ -5,9 +5,9 @@ describe('Browse By Subject', () => {
cy.visit('/browse/subject');
// Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata-page').should('be.visible');
cy.get('ds-browse-by-metadata').should('be.visible');
// Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata-page');
testA11y('ds-browse-by-metadata');
});
});

View File

@@ -5,9 +5,9 @@ describe('Browse By Title', () => {
cy.visit('/browse/title');
// Wait for <ds-browse-by-title-page> to be visible
cy.get('ds-browse-by-title-page').should('be.visible');
cy.get('ds-browse-by-title').should('be.visible');
// Analyze <ds-browse-by-title-page> for accessibility
testA11y('ds-browse-by-title-page');
testA11y('ds-browse-by-title');
});
});

View File

@@ -14,13 +14,8 @@
# Therefore, it should be kept in sync with that file
version: "3.7"
networks:
dspacenet:
services:
dspace-cli:
networks:
dspacenet: {}
environment:
# This assetstore zip is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
- LOADASSETS=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz

View File

@@ -13,7 +13,13 @@
#
# Therefore, it should be kept in sync with that file
version: "3.7"
networks:
# Default to using network named 'dspacenet' from docker-compose-rest.yml.
# Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet")
# If COMPOSITE_PROJECT_NAME is missing, default value will be "docker" (name of folder this file is in)
default:
name: ${COMPOSE_PROJECT_NAME:-docker}_dspacenet
external: true
services:
dspace-cli:
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
@@ -30,16 +36,12 @@ services:
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
solr__P__server: http://dspacesolr:8983/solr
volumes:
- "assetstore:/dspace/assetstore"
# Keep DSpace assetstore directory between reboots
- assetstore:/dspace/assetstore
entrypoint: /dspace/bin/dspace
command: help
networks:
- dspacenet
tty: true
stdin_open: true
volumes:
assetstore:
networks:
dspacenet:

View File

@@ -33,11 +33,11 @@ services:
# Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit.
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
solr__D__statistics__P__autoCommit: 'false'
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
depends_on:
- dspacedb
image: dspace/dspace:latest-test
networks:
dspacenet:
- dspacenet
ports:
- published: 8080
target: 8080
@@ -45,8 +45,6 @@ services:
tty: true
volumes:
- assetstore:/dspace/assetstore
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
- solr_configs:/dspace/solr
# Ensure that the database is ready BEFORE starting tomcat
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
@@ -70,21 +68,18 @@ services:
PGDATA: /pgdata
image: dspace/dspace-postgres-pgcrypto:loadsql
networks:
dspacenet:
- dspacenet
stdin_open: true
tty: true
volumes:
# Keep Postgres data directory between reboots
- pgdata:/pgdata
# DSpace Solr container
dspacesolr:
container_name: dspacesolr
# Uses official Solr image at https://hub.docker.com/_/solr/
image: solr:8.11-slim
# Needs main 'dspace' container to start first to guarantee access to solr_configs
depends_on:
- dspace
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
networks:
dspacenet:
- dspacenet
ports:
- published: 8983
target: 8983
@@ -92,9 +87,6 @@ services:
tty: true
working_dir: /var/solr/data
volumes:
# Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder)
# This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume
- solr_configs:/opt/solr/server/solr/configsets/dspace
# Keep Solr data directory between reboots
- solr_data:/var/solr/data
# Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr
@@ -103,14 +95,18 @@ services:
- '-c'
- |
init-var-solr
precreate-core authority /opt/solr/server/solr/configsets/dspace/authority
precreate-core oai /opt/solr/server/solr/configsets/dspace/oai
precreate-core search /opt/solr/server/solr/configsets/dspace/search
precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics
precreate-core authority /opt/solr/server/solr/configsets/authority
cp -r /opt/solr/server/solr/configsets/authority/* authority
precreate-core oai /opt/solr/server/solr/configsets/oai
cp -r /opt/solr/server/solr/configsets/oai/* oai
precreate-core search /opt/solr/server/solr/configsets/search
cp -r /opt/solr/server/solr/configsets/search/* search
precreate-core statistics /opt/solr/server/solr/configsets/statistics
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
exec solr -f
volumes:
assetstore:
pgdata:
solr_data:
# Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above)
solr_configs:

View File

@@ -43,7 +43,7 @@ services:
depends_on:
- dspacedb
networks:
dspacenet:
- dspacenet
ports:
- published: 8080
target: 8080
@@ -51,8 +51,6 @@ services:
tty: true
volumes:
- assetstore:/dspace/assetstore
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
- solr_configs:/dspace/solr
# Ensure that the database is ready BEFORE starting tomcat
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
# 2. Then, run database migration to init database tables
@@ -69,25 +67,23 @@ services:
container_name: dspacedb
environment:
PGDATA: /pgdata
image: dspace/dspace-postgres-pgcrypto
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
networks:
dspacenet:
- dspacenet
ports:
- published: 5432
target: 5432
stdin_open: true
tty: true
volumes:
# Keep Postgres data directory between reboots
- pgdata:/pgdata
# DSpace Solr container
dspacesolr:
container_name: dspacesolr
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
# Needs main 'dspace' container to start first to guarantee access to solr_configs
depends_on:
- dspace
networks:
dspacenet:
- dspacenet
ports:
- published: 8983
target: 8983
@@ -115,10 +111,10 @@ services:
cp -r /opt/solr/server/solr/configsets/search/* search
precreate-core statistics /opt/solr/server/solr/configsets/statistics
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
exec solr -f
volumes:
assetstore:
pgdata:
solr_data:
# Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above)
solr_configs:

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
/**
* Interface for the route parameters.
*/
export interface AdminNotificationsPublicationClaimPageParams {
pageId?: string;
pageSize?: number;
currentPage?: number;
}
/**
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable()
export class AdminNotificationsPublicationClaimPageResolver implements Resolve<AdminNotificationsPublicationClaimPageParams> {
/**
* Method for resolving the parameters in the current route.
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns AdminNotificationsSuggestionTargetsPageParams Emits the route parameters
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminNotificationsPublicationClaimPageParams {
return {
pageId: route.queryParams.pageId,
pageSize: parseInt(route.queryParams.pageSize, 10),
currentPage: parseInt(route.queryParams.page, 10)
};
}
}

View File

@@ -0,0 +1 @@
<ds-publication-claim [source]="'openaire'"></ds-publication-claim>

View File

@@ -0,0 +1,38 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
describe('AdminNotificationsPublicationClaimPageComponent', () => {
let component: AdminNotificationsPublicationClaimPageComponent;
let fixture: ComponentFixture<AdminNotificationsPublicationClaimPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
TranslateModule.forRoot()
],
declarations: [
AdminNotificationsPublicationClaimPageComponent
],
providers: [
AdminNotificationsPublicationClaimPageComponent
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AdminNotificationsPublicationClaimPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-admin-notifications-publication-claim-page',
templateUrl: './admin-notifications-publication-claim-page.component.html',
styleUrls: ['./admin-notifications-publication-claim-page.component.scss']
})
export class AdminNotificationsPublicationClaimPageComponent {
}

View File

@@ -2,6 +2,7 @@ import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { getNotificationsModuleRoute } from '../admin-routing-paths';
export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance';
export const PUBLICATION_CLAIMS_PATH = 'publication-claim';
export function getQualityAssuranceRoute(id: string) {
return new URLCombiner(getNotificationsModuleRoute(), QUALITY_ASSURANCE_EDIT_PATH, id).toString();

View File

@@ -4,6 +4,9 @@ import { RouterModule } from '@angular/router';
import { AuthenticatedGuard } from '../../core/auth/authenticated.guard';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
import { PUBLICATION_CLAIMS_PATH } from './admin-notifications-routing-paths';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
import { AdminNotificationsPublicationClaimPageResolver } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service';
import { QUALITY_ASSURANCE_EDIT_PATH } from './admin-notifications-routing-paths';
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
@@ -20,6 +23,21 @@ import {
@NgModule({
imports: [
RouterModule.forChild([
{
canActivate: [ AuthenticatedGuard ],
path: `${PUBLICATION_CLAIMS_PATH}`,
component: AdminNotificationsPublicationClaimPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
suggestionTargetParams: AdminNotificationsPublicationClaimPageResolver
},
data: {
title: 'admin.notifications.publicationclaim.page.title',
breadcrumbKey: 'admin.notifications.publicationclaim',
showBreadcrumbsFluid: false
}
},
{
canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
@@ -71,7 +89,9 @@ import {
providers: [
I18nBreadcrumbResolver,
I18nBreadcrumbsService,
AdminNotificationsPublicationClaimPageResolver,
SourceDataResolver,
AdminQualityAssuranceSourcePageResolver,
AdminQualityAssuranceTopicsPageResolver,
AdminQualityAssuranceEventsPageResolver,
AdminQualityAssuranceSourcePageResolver,

View File

@@ -3,10 +3,11 @@ import { NgModule } from '@angular/core';
import { CoreModule } from '../../core/core.module';
import { SharedModule } from '../../shared/shared.module';
import { AdminNotificationsRoutingModule } from './admin-notifications-routing.module';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
import {NotificationsModule} from '../../notifications/notifications.module';
import { NotificationsModule } from '../../notifications/notifications.module';
@NgModule({
imports: [
@@ -17,6 +18,7 @@ import {NotificationsModule} from '../../notifications/notifications.module';
NotificationsModule
],
declarations: [
AdminNotificationsPublicationClaimPageComponent,
AdminQualityAssuranceTopicsPageComponent,
AdminQualityAssuranceEventsPageComponent,
AdminQualityAssuranceSourcePageComponent

View File

@@ -1,4 +1,4 @@
<ng-template dsListableObject>
<ng-template dsDynamicComponentLoader>
</ng-template>
<div #badges>
<ng-content></ng-content>

View File

@@ -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.compRef = viewContainerRef.createComponent(
component, {
index: 0,
injector: undefined,
projectableNodes: [
[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.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;
}
}
/**

View File

@@ -1,4 +1,4 @@
<ng-template dsListableObject>
<ng-template dsDynamicComponentLoader>
</ng-template>
<div #badges class="position-absolute ml-1">
<div class="workflow-badge">

View File

@@ -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(),

View File

@@ -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;
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;
}
}
/**

View File

@@ -1,4 +1,4 @@
<ng-template dsListableObject>
<ng-template dsDynamicComponentLoader>
</ng-template>
<div #badges class="position-absolute ml-1">
<div class="workflow-badge">

View File

@@ -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(),

View File

@@ -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>}

View File

@@ -1,5 +1,5 @@
import { NgModule } from '@angular/core';
import { RouterModule, NoPreloading } from '@angular/router';
import { NoPreloading, RouterModule } from '@angular/router';
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
@@ -38,8 +38,10 @@ import {
ThemedPageInternalServerErrorComponent
} from './page-internal-server-error/themed-page-internal-server-error.component';
import { ServerCheckGuard } from './core/server-check/server-check.guard';
import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths';
import { MenuResolver } from './menu.resolver';
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard';
@NgModule({
imports: [
@@ -94,7 +96,10 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
path: FORGOT_PASSWORD_PATH,
loadChildren: () => import('./forgot-password/forgot-password.module')
.then((m) => m.ForgotPasswordModule),
canActivate: [EndUserAgreementCurrentUserGuard]
canActivate: [
ForgotPasswordCheckGuard,
EndUserAgreementCurrentUserGuard
]
},
{
path: COMMUNITY_MODULE_PATH,
@@ -202,6 +207,11 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
.then((m) => m.ProcessPageModule),
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
},
{ path: SUGGESTION_MODULE_PATH,
loadChildren: () => import('./suggestions-page/suggestions-page.module')
.then((m) => m.SuggestionsPageModule),
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
},
{
path: INFO_MODULE_PATH,
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule)

View File

@@ -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`);
}
}

View File

@@ -1,4 +1,4 @@
import { BrowseByDatePageComponent } from './browse-by-date-page.component';
import { BrowseByDateComponent } from './browse-by-date.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
@@ -15,7 +15,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { Community } from '../../core/shared/community.model';
import { Item } from '../../core/shared/item.model';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec';
import { toRemoteData } from '../browse-by-metadata/browse-by-metadata.component.spec';
import { VarDirective } from '../../shared/utils/var.directive';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PaginationService } from '../../core/pagination/pagination.service';
@@ -23,10 +23,11 @@ import { PaginationServiceStub } from '../../shared/testing/pagination-service.s
import { APP_CONFIG } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
import { SortDirection } from '../../core/cache/models/sort-options.model';
import { cold } from 'jasmine-marbles';
describe('BrowseByDatePageComponent', () => {
let comp: BrowseByDatePageComponent;
let fixture: ComponentFixture<BrowseByDatePageComponent>;
describe('BrowseByDateComponent', () => {
let comp: BrowseByDateComponent;
let fixture: ComponentFixture<BrowseByDateComponent>;
let route: ActivatedRoute;
let paginationService;
@@ -86,7 +87,7 @@ describe('BrowseByDatePageComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [BrowseByDatePageComponent, EnumKeysPipe, VarDirective],
declarations: [BrowseByDateComponent, EnumKeysPipe, VarDirective],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: BrowseService, useValue: mockBrowseService },
@@ -101,7 +102,7 @@ describe('BrowseByDatePageComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByDatePageComponent);
fixture = TestBed.createComponent(BrowseByDateComponent);
const browseService = fixture.debugElement.injector.get(BrowseService);
spyOn(browseService, 'getFirstItemFor')
// ok to expect the default browse as first param since we just need the mock items obtained via sort direction.
@@ -112,9 +113,13 @@ describe('BrowseByDatePageComponent', () => {
fixture.detectChanges();
});
it('should initialize the list of items', () => {
it('should initialize the list of items', (done: DoneFn) => {
expect(comp.loading$).toBeObservable(cold('(a|)', {
a: false,
}));
comp.items$.subscribe((result) => {
expect(result.payload.page).toEqual([firstItem]);
done();
});
});

View File

@@ -1,10 +1,6 @@
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import {
BrowseByMetadataPageComponent,
browseParamsToOptions,
getBrowseSearchOptions
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core';
import { BrowseByMetadataComponent, browseParamsToOptions, getBrowseSearchOptions } from '../browse-by-metadata/browse-by-metadata.component';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { BrowseService } from '../../core/browse/browse.service';
@@ -19,32 +15,36 @@ 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',
styleUrls: ['../browse-by-metadata-page/browse-by-metadata-page.component.scss'],
templateUrl: '../browse-by-metadata-page/browse-by-metadata-page.component.html'
selector: 'ds-browse-by-date',
styleUrls: ['../browse-by-metadata/browse-by-metadata.component.scss'],
templateUrl: '../browse-by-metadata/browse-by-metadata.component.html',
})
/**
* Component for browsing items by metadata definition of type 'date'
* 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 BrowseByDateComponent extends BrowseByMetadataComponent implements OnInit {
/**
* The default metadata keys to use for determining the lower limit of the StartsWith dropdown options
*/
defaultMetadataKeys = ['dc.date.issued'];
public constructor(protected route: ActivatedRoute,
public constructor(
protected route: ActivatedRoute,
protected browseService: BrowseService,
protected dsoService: DSpaceObjectDataService,
protected router: Router,
protected paginationService: PaginationService,
protected cdRef: ChangeDetectorRef,
protected router: Router,
@Inject(APP_CONFIG) public appConfig: AppConfig,
public dsoNameService: DSONameService,
protected cdRef: ChangeDetectorRef,
) {
super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService);
}
@@ -57,19 +57,17 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push(
observableCombineLatest([this.route.params, this.route.queryParams, this.route.data,
observableCombineLatest([this.route.params, this.route.queryParams, this.scope$, this.route.data,
this.currentPagination$, this.currentSort$]).pipe(
map(([routeParams, queryParams, data, currentPage, currentSort]) => {
return [Object.assign({}, routeParams, queryParams, data), currentPage, currentSort];
map(([routeParams, queryParams, scope, data, currentPage, currentSort]) => {
return [Object.assign({}, routeParams, queryParams, data), scope, currentPage, currentSort];
})
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
const metadataKeys = params.browseDefinition ? params.browseDefinition.metadataKeys : this.defaultMetadataKeys;
this.browseId = params.id || this.defaultBrowseId;
this.startsWith = +params.startsWith || params.startsWith;
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails);
const searchOptions = browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, this.fetchThumbnails);
this.updatePageWithItems(searchOptions, this.value, undefined);
this.updateParent(params.scope);
this.updateLogo();
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
}));
}
@@ -85,12 +83,21 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
* @param scope The scope under which to fetch the earliest item for
*/
updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) {
const firstItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.ASC);
const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
const firstItemRD$: Observable<RemoteData<Item>> = this.browseService.getFirstItemFor(definition, scope, SortDirection.ASC);
const lastItemRD$: Observable<RemoteData<Item>> = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
this.loading$ = observableCombineLatest([
firstItemRD$,
lastItemRD$,
]).pipe(
map(([firstItemRD, lastItemRD]: [RemoteData<Item>, RemoteData<Item>]) => firstItemRD.isLoading || lastItemRD.isLoading)
);
this.subs.push(
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
let lowerLimit: number = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
let upperLimit: number = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
observableCombineLatest([
firstItemRD$,
lastItemRD$,
]).subscribe(([firstItemRD, lastItemRD]: [RemoteData<Item>, RemoteData<Item>]) => {
let lowerLimit: number = this.getLimit(firstItemRD, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
let upperLimit: number = this.getLimit(lastItemRD, metadataKeys, new Date().getUTCFullYear());
const options: number[] = [];
const oneYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
const fiveYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;

View File

@@ -1,17 +1,13 @@
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';
import { RouterStub } from '../shared/testing/router.stub';
describe('BrowseByGuard', () => {
describe('canActivate', () => {
let guard: BrowseByGuard;
let dsoService: any;
let translateService: any;
let browseDefinitionService: any;
let router: any;
@@ -25,10 +21,6 @@ describe('BrowseByGuard', () => {
const browseDefinition = Object.assign(new ValueListBrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] });
beforeEach(() => {
dsoService = {
findById: (dsoId: string) => observableOf({ payload: { name: name }, hasSucceeded: true })
};
translateService = {
instant: () => field
};
@@ -39,7 +31,7 @@ describe('BrowseByGuard', () => {
router = new RouterStub() as any;
guard = new BrowseByGuard(dsoService, translateService, browseDefinitionService, new DSONameServiceMock() as DSONameService, router);
guard = new BrowseByGuard(translateService, browseDefinitionService, router);
});
it('should return true, and sets up the data correctly, with a scope and value', () => {
@@ -48,6 +40,7 @@ describe('BrowseByGuard', () => {
title: field,
browseDefinition,
},
parent: null,
params: {
id,
},
@@ -64,7 +57,7 @@ describe('BrowseByGuard', () => {
title,
id,
browseDefinition,
collection: name,
scope,
field,
value: '"' + value + '"'
};
@@ -97,7 +90,7 @@ describe('BrowseByGuard', () => {
title,
id,
browseDefinition,
collection: name,
scope,
field,
value: ''
};
@@ -108,12 +101,48 @@ describe('BrowseByGuard', () => {
);
});
it('should return true, and sets up the data correctly using the community/collection page id, with a scope and without value', () => {
const scopedNoValueRoute = {
data: {
title: field,
browseDefinition,
},
parent: {
params: {
id: scope,
},
},
params: {
id,
},
queryParams: {
},
};
guard.canActivate(scopedNoValueRoute as any, undefined).pipe(
first(),
).subscribe((canActivate) => {
const result = {
title,
id,
browseDefinition,
scope,
field,
value: '',
};
expect(scopedNoValueRoute.data).toEqual(result);
expect(router.navigate).not.toHaveBeenCalled();
expect(canActivate).toEqual(true);
});
});
it('should return true, and sets up the data correctly, without a scope and with a value', () => {
const route = {
data: {
title: field,
browseDefinition,
},
parent: null,
params: {
id,
},
@@ -129,7 +158,7 @@ describe('BrowseByGuard', () => {
title,
id,
browseDefinition,
collection: '',
scope: undefined,
field,
value: '"' + value + '"'
};
@@ -147,6 +176,7 @@ describe('BrowseByGuard', () => {
data: {
title: field,
},
parent: null,
params: {
id,
},

View File

@@ -1,15 +1,12 @@
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, Data } from '@angular/router';
import { Injectable } from '@angular/core';
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
import { hasNoValue, hasValue } from '../shared/empty.util';
import { map, switchMap } from 'rxjs/operators';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs';
import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service';
import { BrowseDefinition } from '../core/shared/browse-definition.model';
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { RemoteData } from '../core/data/remote-data';
import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths';
@@ -19,10 +16,9 @@ import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths';
*/
export class BrowseByGuard implements CanActivate {
constructor(protected dsoService: DSpaceObjectDataService,
constructor(
protected translate: TranslateService,
protected browseDefinitionService: BrowseDefinitionDataService,
protected dsoNameService: DSONameService,
protected router: Router,
) {
}
@@ -39,25 +35,14 @@ export class BrowseByGuard implements CanActivate {
} else {
browseDefinition$ = observableOf(route.data.browseDefinition);
}
const scope = route.queryParams.scope;
const scope = route.queryParams.scope ?? route.parent?.params.id;
const value = route.queryParams.value;
const metadataTranslated = this.translate.instant(`browse.metadata.${id}`);
return browseDefinition$.pipe(
switchMap((browseDefinition: BrowseDefinition | undefined) => {
if (hasValue(browseDefinition)) {
if (hasValue(scope)) {
const dso$: Observable<DSpaceObject> = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteDataPayload());
return dso$.pipe(
map((dso: DSpaceObject) => {
const name = this.dsoNameService.getName(dso);
route.data = this.createData(title, id, browseDefinition, name, metadataTranslated, value, route);
return true;
})
);
} else {
route.data = this.createData(title, id, browseDefinition, '', metadataTranslated, value, route);
route.data = this.createData(title, id, browseDefinition, metadataTranslated, value, route, scope);
return observableOf(true);
}
} else {
void this.router.navigate([PAGE_NOT_FOUND_PATH]);
return observableOf(false);
@@ -66,14 +51,14 @@ export class BrowseByGuard implements CanActivate {
);
}
private createData(title, id, browseDefinition, collection, field, value, route) {
private createData(title: string, id: string, browseDefinition: BrowseDefinition, field: string, value: string, route: ActivatedRouteSnapshot, scope: string): Data {
return Object.assign({}, route.data, {
title: title,
id: id,
browseDefinition: browseDefinition,
collection: collection,
field: field,
value: hasValue(value) ? `"${value}"` : ''
value: hasValue(value) ? `"${value}"` : '',
scope: scope,
});
}
}

View File

@@ -1,67 +0,0 @@
<div class="container">
<ng-container *ngVar="(parent$ | async) as parent">
<ng-container *ngIf="parent?.payload as parentContext">
<div class="d-flex flex-row border-bottom mb-4 pb-4">
<header class="comcol-header mr-auto">
<!-- Parent Name -->
<ds-comcol-page-header [name]="dsoNameService.getName(parentContext)">
</ds-comcol-page-header>
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logo$"
[logo]="(logo$ | async)?.payload"
[alternateText]="'Community or Collection Logo'">
</ds-comcol-page-logo>
<!-- Handle -->
<ds-themed-comcol-page-handle
[content]="parentContext.handle"
[title]="parentContext.type+'.page.handle'" >
</ds-themed-comcol-page-handle>
<!-- Introductory text -->
<ds-comcol-page-content [content]="parentContext.introductoryText" [hasInnerHtml]="true">
</ds-comcol-page-content>
<!-- News -->
<ds-comcol-page-content [content]="parentContext.sidebarText" [hasInnerHtml]="true" [title]="'community.page.news'">
</ds-comcol-page-content>
</header>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<!-- Browse-By Links -->
<ds-themed-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-themed-comcol-page-browse-by>
</ng-container></ng-container>
<section class="comcol-page-browse-section">
<div class="browse-by-metadata w-100">
<ds-themed-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
title="{{'browse.title' | translate:
{
collection: dsoNameService.getName((parent$ | async)?.payload),
field: 'browse.metadata.' + browseId | translate,
startsWith: (startsWith)? ('browse.startsWith' | translate: { startsWith: '&quot;' + startsWith + '&quot;' }) : '',
value: (value)? '&quot;' + value + '&quot;': ''
} }}"
parentname="{{dsoNameService.getName((parent$ | async)?.payload)}}"
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
[paginationConfig]="(currentPagination$ |async)"
[sortConfig]="(currentSort$ |async)"
[type]="startsWithType"
[startsWithOptions]="startsWithOptions"
(prev)="goPrev()"
(next)="goNext()">
</ds-themed-browse-by>
<ds-themed-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
</div>
</section>
<ng-container *ngVar="(parent$ | async) as parent">
<ng-container *ngIf="parent?.payload as parentContext">
<footer *ngIf="parentContext.copyrightText" class="border-top my-5 pt-4">
<div >
<!-- Copyright -->
<ds-comcol-page-content [content]="parentContext.copyrightText" [hasInnerHtml]="true">
</ds-comcol-page-content>
</div>
</footer>
</ng-container>
</ng-container>
</div>

View File

@@ -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`);
}
}

View File

@@ -0,0 +1,21 @@
<section class="comcol-page-browse-section">
<div class="browse-by-metadata w-100">
<ds-themed-browse-by *ngIf="!(loading$ | async)" class="col-xs-12 w-100"
title="{{'browse.title' | translate:{
field: 'browse.metadata.' + browseId | translate,
startsWith: (startsWith)? ('browse.startsWith' | translate: { startsWith: '&quot;' + startsWith + '&quot;' }) : '',
value: (value)? '&quot;' + value + '&quot;': ''
} }}"
[displayTitle]="displayTitle"
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
[paginationConfig]="(currentPagination$ |async)"
[sortConfig]="(currentSort$ |async)"
[type]="startsWithType"
[startsWithOptions]="startsWithOptions"
(prev)="goPrev()"
(next)="goNext()">
</ds-themed-browse-by>
<ds-themed-loading *ngIf="loading$ | async"
message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
</div>
</section>

View File

@@ -1,8 +1,8 @@
import {
BrowseByMetadataPageComponent,
BrowseByMetadataComponent,
browseParamsToOptions,
getBrowseSearchOptions
} from './browse-by-metadata-page.component';
} from './browse-by-metadata.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { BrowseService } from '../../core/browse/browse.service';
import { CommonModule } from '@angular/common';
@@ -30,10 +30,11 @@ import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { APP_CONFIG } from '../../../config/app-config.interface';
import { cold } from 'jasmine-marbles';
describe('BrowseByMetadataPageComponent', () => {
let comp: BrowseByMetadataPageComponent;
let fixture: ComponentFixture<BrowseByMetadataPageComponent>;
describe('BrowseByMetadataComponent', () => {
let comp: BrowseByMetadataComponent;
let fixture: ComponentFixture<BrowseByMetadataComponent>;
let browseService: BrowseService;
let route: ActivatedRoute;
let paginationService;
@@ -103,7 +104,7 @@ describe('BrowseByMetadataPageComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [BrowseByMetadataPageComponent, EnumKeysPipe, VarDirective],
declarations: [BrowseByMetadataComponent, EnumKeysPipe, VarDirective],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: BrowseService, useValue: mockBrowseService },
@@ -117,7 +118,7 @@ describe('BrowseByMetadataPageComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByMetadataPageComponent);
fixture = TestBed.createComponent(BrowseByMetadataComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
browseService = (comp as any).browseService;
@@ -144,20 +145,18 @@ describe('BrowseByMetadataPageComponent', () => {
route.params = observableOf(paramsWithValue);
comp.ngOnInit();
comp.updateParent('fake-scope');
comp.updateLogo();
fixture.detectChanges();
});
it('should fetch items', () => {
it('should fetch items', (done: DoneFn) => {
expect(comp.loading$).toBeObservable(cold('(a|)', {
a: false,
}));
comp.items$.subscribe((result) => {
expect(result.payload.page).toEqual(mockItems);
done();
});
});
it('should fetch the logo', () => {
expect(comp.logo$).toBeTruthy();
});
});
describe('when calling browseParamsToOptions', () => {
@@ -176,7 +175,7 @@ describe('BrowseByMetadataPageComponent', () => {
field: 'fake-field',
};
result = browseParamsToOptions(paramsScope, paginationOptions, sortOptions, 'author', comp.fetchThumbnails);
result = browseParamsToOptions(paramsScope, 'fake-scope', paginationOptions, sortOptions, 'author', comp.fetchThumbnails);
});
it('should return BrowseEntrySearchOptions with the correct properties', () => {

View File

@@ -1,5 +1,5 @@
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { Component, Inject, OnInit, OnDestroy } from '@angular/core';
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription, of as observableOf } from 'rxjs';
import { Component, Inject, OnInit, OnDestroy, Input, OnChanges } 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';
@@ -12,23 +12,21 @@ import { Item } from '../../core/shared/item.model';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { getFirstSucceededRemoteData } from '../../core/shared/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
import { PaginationService } from '../../core/pagination/pagination.service';
import { filter, map, mergeMap } from 'rxjs/operators';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { Bitstream } from '../../core/shared/bitstream.model';
import { Collection } from '../../core/shared/collection.model';
import { Community } from '../../core/shared/community.model';
import { map } from 'rxjs/operators';
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';
@Component({
selector: 'ds-browse-by-metadata-page',
styleUrls: ['./browse-by-metadata-page.component.scss'],
templateUrl: './browse-by-metadata-page.component.html'
selector: 'ds-browse-by-metadata',
styleUrls: ['./browse-by-metadata.component.scss'],
templateUrl: './browse-by-metadata.component.html',
})
/**
* Component for browsing (items) by metadata definition.
@@ -36,7 +34,30 @@ export const BBM_PAGINATION_ID = 'bbm';
* or multiple metadata fields. An example would be 'author' for
* 'dc.contributor.*'
*/
export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
@rendersBrowseBy(BrowseByDataType.Metadata)
export class BrowseByMetadataComponent implements OnInit, OnChanges, OnDestroy {
/**
* The optional context
*/
@Input() context: Context;
/**
* The {@link BrowseByDataType} of this Component
*/
@Input() browseByType: BrowseByDataType;
/**
* The ID of the {@link Community} or {@link Collection} of the scope to display
*/
@Input() scope: string;
/**
* Display the h1 title in the section
*/
@Input() displayTitle = true;
scope$: BehaviorSubject<string> = new BehaviorSubject(undefined);
/**
* The list of browse-entries to display
@@ -48,16 +69,6 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
*/
items$: Observable<RemoteData<PaginatedList<Item>>>;
/**
* The current Community or Collection we're browsing metadata/items in
*/
parent$: Observable<RemoteData<DSpaceObject>>;
/**
* The logo of the current Community or Collection
*/
logo$: Observable<RemoteData<Bitstream>>;
/**
* The pagination config used to display the values
*/
@@ -98,7 +109,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
* The list of StartsWith options
* Should be defined after ngOnInit is called!
*/
startsWithOptions;
startsWithOptions: (string | number)[];
/**
* The value we're browsing items for
@@ -122,6 +133,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
*/
fetchThumbnails: boolean;
/**
* Observable determining if the loading animation needs to be shown
*/
loading$ = observableOf(true);
public constructor(protected route: ActivatedRoute,
protected browseService: BrowseService,
protected dsoService: DSpaceObjectDataService,
@@ -130,7 +146,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,
@@ -147,11 +162,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push(
observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe(
map(([routeParams, queryParams, currentPage, currentSort]) => {
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
observableCombineLatest([this.route.params, this.route.queryParams, this.scope$, this.currentPagination$, this.currentSort$]).pipe(
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
})
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
this.browseId = params.id || this.defaultBrowseId;
this.authority = params.authority;
@@ -170,18 +185,19 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
}
if (isNotEmpty(this.value)) {
this.updatePageWithItems(
browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
this.updatePageWithItems(browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, this.fetchThumbnails), this.value, this.authority);
} else {
this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false));
this.updatePage(browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, false));
}
this.updateParent(params.scope);
this.updateLogo();
}));
this.updateStartsWithTextOptions();
}
ngOnChanges(): void {
this.scope$.next(this.scope);
}
/**
* Update the StartsWith options with text values
* It adds the value "0-9" as well as all letters from A to Z
@@ -200,6 +216,9 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
*/
updatePage(searchOptions: BrowseEntrySearchOptions) {
this.browseEntries$ = this.browseService.getBrowseEntriesFor(searchOptions);
this.loading$ = this.browseEntries$.pipe(
map((browseEntriesRD: RemoteData<PaginatedList<BrowseEntry>>) => browseEntriesRD.isLoading),
);
this.items$ = undefined;
}
@@ -214,38 +233,10 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
*/
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string, authority: string) {
this.items$ = this.browseService.getBrowseItemsFor(value, authority, searchOptions);
}
/**
* Update the parent Community or Collection using their scope
* @param scope The UUID of the Community or Collection to fetch
*/
updateParent(scope: string) {
if (hasValue(scope)) {
const linksToFollow = () => {
return [followLink('logo')];
};
this.parent$ = this.dsoService.findById(scope,
true,
true,
...linksToFollow() as FollowLinkConfig<DSpaceObject>[]).pipe(
getFirstSucceededRemoteData()
this.loading$ = this.items$.pipe(
map((itemsRD: RemoteData<PaginatedList<Item>>) => itemsRD.isLoading),
);
}
}
/**
* Update the parent Community or Collection logo
*/
updateLogo() {
if (hasValue(this.parent$)) {
this.logo$ = this.parent$.pipe(
map((rd: RemoteData<Collection | Community>) => rd.payload),
filter((collectionOrCommunity: Collection | Community) => hasValue(collectionOrCommunity.logo)),
mergeMap((collectionOrCommunity: Collection | Community) => collectionOrCommunity.logo)
);
}
}
/**
* Navigate to the previous page
@@ -278,7 +269,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);
}
@@ -307,12 +298,14 @@ export function getBrowseSearchOptions(defaultBrowseId: string,
/**
* Function to transform query and url parameters into searchOptions used to fetch browse entries or items
* @param params URL and query parameters
* @param scope The scope to show the results
* @param paginationConfig Pagination configuration
* @param sortConfig Sorting configuration
* @param metadata Optional metadata definition to fetch browse entries/items for
* @param fetchThumbnail Optional parameter for requesting thumbnail images
*/
export function browseParamsToOptions(params: any,
scope: string,
paginationConfig: PaginationComponentOptions,
sortConfig: SortOptions,
metadata?: string,
@@ -322,7 +315,7 @@ export function browseParamsToOptions(params: any,
paginationConfig,
sortConfig,
params.startsWith,
params.scope,
scope,
fetchThumbnail
);
}

View File

@@ -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 {

View File

@@ -0,0 +1,4 @@
<div class="container">
<ds-browse-by-switcher [browseByType]="browseByType$ | async">
</ds-browse-by-switcher>
</div>

View File

@@ -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();
});
});

View 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()),
);
}
}

View File

@@ -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' }

View File

@@ -0,0 +1,6 @@
export enum BrowseByDataType {
Title = 'title',
Metadata = 'text',
Date = 'date',
Hierarchy = 'hierarchy',
}

View File

@@ -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);

View File

@@ -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)) {

View File

@@ -1 +0,0 @@
<ng-container *ngComponentOutlet="browseByComponent | async"></ng-container>

View File

@@ -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();
});
});
});

View File

@@ -1,38 +1,38 @@
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()))
);
@Input() displayTitle: boolean;
@Input() scope: string;
protected inputNamesDependentForComponent: (keyof this & string)[] = [
'context',
'browseByType',
];
protected inputNames: (keyof this & string)[] = [
'context',
'browseByType',
'displayTitle',
'scope',
];
public getComponent(): GenericConstructor<Component> {
return getComponentByBrowseByType(this.browseByType, this.context, this.themeService.getThemeName());
}
}

View File

@@ -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,

View File

@@ -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`);
}
}

View File

@@ -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`);
}
}

View File

@@ -1,5 +1,11 @@
<div class="container">
<h1>{{ ('browse.taxonomy_' + vocabularyName + '.title') | translate }}</h1>
<section>
<h1 *ngIf="displayTitle">
{{ ('browse.title') | translate:{
field: 'browse.metadata.' + vocabularyName | translate,
startsWith: '',
value: '',
} }}
</h1>
<div class="mb-3">
<ds-vocabulary-treeview [vocabularyOptions]=vocabularyOptions
[multiSelect]="true"
@@ -12,4 +18,4 @@
[queryParams]="queryParams"
[queryParamsHandling]="'merge'">
{{ 'browse.taxonomy.button' | translate }}</a>
</div>
</section>

View File

@@ -1,6 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component';
import { BrowseByTaxonomyComponent } from './browse-by-taxonomy.component';
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
@@ -10,9 +9,9 @@ import { createDataWithBrowseDefinition } from '../browse-by-switcher/browse-by-
import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model';
import { ThemeService } from '../../shared/theme-support/theme.service';
describe('BrowseByTaxonomyPageComponent', () => {
let component: BrowseByTaxonomyPageComponent;
let fixture: ComponentFixture<BrowseByTaxonomyPageComponent>;
describe('BrowseByTaxonomyComponent', () => {
let component: BrowseByTaxonomyComponent;
let fixture: ComponentFixture<BrowseByTaxonomyComponent>;
let themeService: ThemeService;
let detail1: VocabularyEntryDetail;
let detail2: VocabularyEntryDetail;
@@ -29,7 +28,9 @@ describe('BrowseByTaxonomyPageComponent', () => {
await TestBed.configureTestingModule({
imports: [ TranslateModule.forRoot() ],
declarations: [ BrowseByTaxonomyPageComponent ],
declarations: [
BrowseByTaxonomyComponent,
],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: ThemeService, useValue: themeService },
@@ -40,8 +41,9 @@ describe('BrowseByTaxonomyPageComponent', () => {
});
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByTaxonomyPageComponent);
fixture = TestBed.createComponent(BrowseByTaxonomyComponent);
component = fixture.componentInstance;
spyOn(component, 'updateQueryParams').and.callThrough();
fixture.detectChanges();
detail1 = new VocabularyEntryDetail();
detail2 = new VocabularyEntryDetail();
@@ -61,6 +63,7 @@ describe('BrowseByTaxonomyPageComponent', () => {
expect(component.selectedItems).toContain(detail1);
expect(component.selectedItems.length).toBe(1);
expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals'] );
expect(component.updateQueryParams).toHaveBeenCalled();
});
it('should handle select event with multiple selected items', () => {
@@ -70,6 +73,7 @@ describe('BrowseByTaxonomyPageComponent', () => {
expect(component.selectedItems).toContain(detail1, detail2);
expect(component.selectedItems.length).toBe(2);
expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'] );
expect(component.updateQueryParams).toHaveBeenCalled();
});
it('should handle deselect event', () => {
@@ -82,6 +86,33 @@ describe('BrowseByTaxonomyPageComponent', () => {
expect(component.selectedItems).toContain(detail2);
expect(component.selectedItems.length).toBe(1);
expect(component.filterValues).toEqual(['TECHNOLOGY,equals'] );
expect(component.updateQueryParams).toHaveBeenCalled();
});
describe('updateQueryParams', () => {
beforeEach(() => {
component.facetType = 'subject';
component.filterValues = ['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'];
});
it('should update the queryParams with the selected filterValues', () => {
component.updateQueryParams();
expect(component.queryParams).toEqual({
'f.subject': ['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'],
});
});
it('should include the scope if present', () => {
component.scope = '67f849f1-2499-4872-8c61-9e2b47d71068';
component.updateQueryParams();
expect(component.queryParams).toEqual({
'f.subject': ['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'],
'scope': '67f849f1-2499-4872-8c61-9e2b47d71068',
});
});
});
afterEach(() => {

View File

@@ -1,24 +1,48 @@
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
import { Component, OnInit, OnChanges, 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 { ActivatedRoute, Params } from '@angular/router';
import { BehaviorSubject, 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';
import { hasValue } from '../../shared/empty.util';
@Component({
selector: 'ds-browse-by-taxonomy-page',
templateUrl: './browse-by-taxonomy-page.component.html',
styleUrls: ['./browse-by-taxonomy-page.component.scss']
selector: 'ds-browse-by-taxonomy',
templateUrl: './browse-by-taxonomy.component.html',
styleUrls: ['./browse-by-taxonomy.component.scss'],
})
/**
* Component for browsing items by metadata in a hierarchical controlled vocabulary
*/
export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
@rendersBrowseBy(BrowseByDataType.Hierarchy)
export class BrowseByTaxonomyComponent implements OnInit, OnChanges, OnDestroy {
/**
* The optional context
*/
@Input() context: Context;
/**
* The {@link BrowseByDataType} of this Component
*/
@Input() browseByType: BrowseByDataType;
/**
* The ID of the {@link Community} or {@link Collection} of the scope to display
*/
@Input() scope: string;
/**
* Display the h1 title in the section
*/
@Input() displayTitle = true;
scope$: BehaviorSubject<string> = new BehaviorSubject(undefined);
/**
* The {@link VocabularyOptions} object
@@ -48,35 +72,41 @@ export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
/**
* The parameters used in the URL
*/
queryParams: any;
queryParams: Params;
/**
* 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 };
}));
this.subs.push(this.scope$.subscribe(() => {
this.updateQueryParams();
}));
}
ngOnChanges(): void {
this.scope$.next(this.scope);
}
/**
@@ -98,21 +128,28 @@ export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
* @param detail VocabularyEntryDetail to be removed
*/
onDeselect(detail: VocabularyEntryDetail): void {
this.selectedItems = this.selectedItems.filter((entry: VocabularyEntryDetail) => { return entry.id !== detail.id; });
this.filterValues = this.filterValues.filter((value: string) => { return value !== `${detail.value},equals`; });
this.selectedItems = this.selectedItems.filter((entry: VocabularyEntryDetail) => {
return entry.id !== detail.id;
});
this.filterValues = this.filterValues.filter((value: string) => {
return value !== `${detail.value},equals`;
});
this.updateQueryParams();
}
/**
* Updates queryParams based on the current facetType and filterValues.
*/
private updateQueryParams(): void {
updateQueryParams(): void {
this.queryParams = {
['f.' + this.facetType]: this.filterValues
};
if (hasValue(this.scope)) {
this.queryParams.scope = this.scope;
}
}
ngOnDestroy(): void {
this.browseByComponentSubs.forEach((sub: Subscription) => sub.unsubscribe());
this.subs.forEach((sub: Subscription) => sub.unsubscribe());
}
}

View File

@@ -1,64 +0,0 @@
import { combineLatest as observableCombineLatest } from 'rxjs';
import { Component, Inject } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { hasValue } from '../../shared/empty.util';
import {
BrowseByMetadataPageComponent,
browseParamsToOptions, getBrowseSearchOptions
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { BrowseService } from '../../core/browse/browse.service';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationService } from '../../core/pagination/pagination.service';
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';
@Component({
selector: 'ds-browse-by-title-page',
styleUrls: ['../browse-by-metadata-page/browse-by-metadata-page.component.scss'],
templateUrl: '../browse-by-metadata-page/browse-by-metadata-page.component.html'
})
/**
* Component for browsing items by title (dc.title)
*/
export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
public constructor(protected route: ActivatedRoute,
protected browseService: BrowseService,
protected dsoService: DSpaceObjectDataService,
protected paginationService: PaginationService,
protected router: Router,
@Inject(APP_CONFIG) public appConfig: AppConfig,
public dsoNameService: DSONameService,
) {
super(route, browseService, dsoService, paginationService, router, appConfig, dsoNameService);
}
ngOnInit(): void {
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
// include the thumbnail configuration in browse search options
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push(
observableCombineLatest([this.route.params, this.route.queryParams, this.currentPagination$, this.currentSort$]).pipe(
map(([routeParams, queryParams, currentPage, currentSort]) => {
return [Object.assign({}, routeParams, queryParams),currentPage,currentSort];
})
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
this.startsWith = +params.startsWith || params.startsWith;
this.browseId = params.id || this.defaultBrowseId;
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
this.updateParent(params.scope);
this.updateLogo();
}));
this.updateStartsWithTextOptions();
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -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`);
}
}

View File

@@ -9,8 +9,8 @@ import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec';
import { BrowseByTitlePageComponent } from './browse-by-title-page.component';
import { toRemoteData } from '../browse-by-metadata/browse-by-metadata.component.spec';
import { BrowseByTitleComponent } from './browse-by-title.component';
import { ItemDataService } from '../../core/data/item-data.service';
import { Community } from '../../core/shared/community.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
@@ -24,9 +24,9 @@ import { APP_CONFIG } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
describe('BrowseByTitlePageComponent', () => {
let comp: BrowseByTitlePageComponent;
let fixture: ComponentFixture<BrowseByTitlePageComponent>;
describe('BrowseByTitleComponent', () => {
let comp: BrowseByTitleComponent;
let fixture: ComponentFixture<BrowseByTitleComponent>;
let itemDataService: ItemDataService;
let route: ActivatedRoute;
@@ -71,7 +71,7 @@ describe('BrowseByTitlePageComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [BrowseByTitlePageComponent, EnumKeysPipe, VarDirective],
declarations: [BrowseByTitleComponent, EnumKeysPipe, VarDirective],
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: BrowseService, useValue: mockBrowseService },
@@ -85,7 +85,7 @@ describe('BrowseByTitlePageComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByTitlePageComponent);
fixture = TestBed.createComponent(BrowseByTitleComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
itemDataService = (comp as any).itemDataService;

View File

@@ -0,0 +1,44 @@
import { combineLatest as observableCombineLatest } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { Params } from '@angular/router';
import {
BrowseByMetadataComponent,
browseParamsToOptions, getBrowseSearchOptions
} from '../browse-by-metadata/browse-by-metadata.component';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
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',
styleUrls: ['../browse-by-metadata/browse-by-metadata.component.scss'],
templateUrl: '../browse-by-metadata/browse-by-metadata.component.html'
})
/**
* Component for browsing items by title (dc.title)
*/
@rendersBrowseBy(BrowseByDataType.Title)
export class BrowseByTitleComponent extends BrowseByMetadataComponent implements OnInit {
ngOnInit(): void {
const sortConfig = new SortOptions('dc.title', SortDirection.ASC);
// include the thumbnail configuration in browse search options
this.updatePage(getBrowseSearchOptions(this.defaultBrowseId, this.paginationConfig, sortConfig, this.fetchThumbnails));
this.currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
this.currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, sortConfig);
this.subs.push(
observableCombineLatest([this.route.params, this.route.queryParams, this.scope$, this.currentPagination$, this.currentSort$]).pipe(
map(([routeParams, queryParams, scope, currentPage, currentSort]) => {
return [Object.assign({}, routeParams, queryParams), scope, currentPage, currentSort];
})
).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => {
this.startsWith = +params.startsWith || params.startsWith;
this.browseId = params.id || this.defaultBrowseId;
this.updatePageWithItems(browseParamsToOptions(params, scope, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined);
}));
this.updateStartsWithTextOptions();
}
}

View File

@@ -1,50 +1,42 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-title-page.component';
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
import { BrowseByTitleComponent } from './browse-by-title/browse-by-title.component';
import { BrowseByMetadataComponent } from './browse-by-metadata/browse-by-metadata.component';
import { BrowseByDateComponent } from './browse-by-date/browse-by-date.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 { BrowseByTaxonomyComponent } from './browse-by-taxonomy/browse-by-taxonomy.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,
BrowseByTitleComponent,
BrowseByMetadataComponent,
BrowseByDateComponent,
BrowseByTaxonomyComponent,
];
@NgModule({
imports: [
SharedBrowseByModule,
CommonModule,
ComcolModule,
DsoPageModule,
FormModule,
SharedModule,
],
declarations: [
BrowseBySwitcherComponent,
ThemedBrowseBySwitcherComponent,
...DECLARATIONS,
...ENTRY_COMPONENTS
],
exports: [
BrowseBySwitcherComponent
...DECLARATIONS,
...ENTRY_COMPONENTS,
]
})
export class BrowseByModule {

View File

@@ -22,6 +22,10 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
import { MenuItemType } from '../shared/menu/menu-item-type.model';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
import { BrowseByGuard } from '../browse-by/browse-by-guard';
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component';
@NgModule({
imports: [
@@ -65,7 +69,23 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
{
path: '',
component: ThemedCollectionPageComponent,
children: [
{
path: '',
pathMatch: 'full',
component: CollectionRecentlyAddedComponent,
},
{
path: 'browse/:id',
pathMatch: 'full',
component: ComcolBrowseByComponent,
canActivate: [BrowseByGuard],
resolve: {
breadcrumb: BrowseByI18nBreadcrumbResolver,
},
data: { breadcrumbKey: 'browse.metadata' },
},
],
}
],
data: {

View File

@@ -19,7 +19,7 @@
<!-- Handle -->
<ds-themed-comcol-page-handle
[content]="collection.handle"
[title]="'collection.page.handle'" >
[title]="'collection.page.handle'">
</ds-themed-comcol-page-handle>
<!-- Introductory text -->
<ds-comcol-page-content
@@ -42,24 +42,7 @@
[contentType]="collection.type">
</ds-themed-comcol-page-browse-by>
<ng-container *ngVar="(itemRD$ | async) as itemRD">
<div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn>
<h3 class="sr-only">{{'collection.page.browse.recent.head' | translate}}</h3>
<ds-viewable-collection
[config]="paginationConfig"
[sortConfig]="sortConfig"
[objects]="itemRD"
[hideGear]="true">
</ds-viewable-collection>
</div>
<ds-error *ngIf="itemRD?.hasFailed"
message="{{'error.recent-submissions' | translate}}"></ds-error>
<ds-themed-loading *ngIf="!itemRD || itemRD.isLoading"
message="{{'loading.recent-submissions' | translate}}"></ds-themed-loading>
<div *ngIf="!itemRD?.isLoading && itemRD?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
{{'collection.page.browse.recent.empty' | translate}}
</div>
</ng-container>
<router-outlet></router-outlet>
</section>
<footer *ngIf="collection.copyrightText" class="border-top my-5 pt-4">
<!-- Copyright -->

View File

@@ -1,36 +1,21 @@
import { ChangeDetectionStrategy, Component, OnInit, Inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../shared/search/models/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { CollectionDataService } from '../core/data/collection-data.service';
import { PaginatedList } from '../core/data/paginated-list.model';
import { Observable } from 'rxjs';
import { filter, map, mergeMap, take } from 'rxjs/operators';
import { SortOptions } from '../core/cache/models/sort-options.model';
import { RemoteData } from '../core/data/remote-data';
import { Bitstream } from '../core/shared/bitstream.model';
import { Collection } from '../core/shared/collection.model';
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
import { Item } from '../core/shared/item.model';
import {
getAllSucceededRemoteDataPayload,
getFirstSucceededRemoteData,
toDSpaceObjectListRD
} from '../core/shared/operators';
import { getAllSucceededRemoteDataPayload } from '../core/shared/operators';
import { fadeIn, fadeInOut } from '../shared/animations/fade';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { AuthService } from '../core/auth/auth.service';
import { PaginationService } from '../core/pagination/pagination.service';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { getCollectionPageRoute } from './collection-page-routing-paths';
import { redirectOn4xx } from '../core/shared/authorized.operators';
import { BROWSE_LINKS_TO_FOLLOW } from '../core/browse/browse.service';
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../src/config/app-config.interface';
@Component({
selector: 'ds-collection-page',
@@ -44,14 +29,9 @@ import { APP_CONFIG, AppConfig } from '../../../src/config/app-config.interface'
})
export class CollectionPageComponent implements OnInit {
collectionRD$: Observable<RemoteData<Collection>>;
itemRD$: Observable<RemoteData<PaginatedList<Item>>>;
logoRD$: Observable<RemoteData<Bitstream>>;
paginationConfig: PaginationComponentOptions;
sortConfig: SortOptions;
private paginationChanges$: Subject<{
paginationConfig: PaginationComponentOptions,
sortConfig: SortOptions
}>;
/**
* Whether the current user is a Community admin
@@ -64,23 +44,12 @@ export class CollectionPageComponent implements OnInit {
collectionPageRoute$: Observable<string>;
constructor(
private collectionDataService: CollectionDataService,
private searchService: SearchService,
private route: ActivatedRoute,
private router: Router,
private authService: AuthService,
private paginationService: PaginationService,
private authorizationDataService: AuthorizationDataService,
protected route: ActivatedRoute,
protected router: Router,
protected authService: AuthService,
protected authorizationDataService: AuthorizationDataService,
public dsoNameService: DSONameService,
@Inject(APP_CONFIG) public appConfig: AppConfig,
) {
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
id: 'cp',
currentPage: 1,
pageSize: this.appConfig.browseBy.pageSize,
});
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
}
ngOnInit(): void {
@@ -96,33 +65,6 @@ export class CollectionPageComponent implements OnInit {
);
this.isCollectionAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCollectionAdmin);
this.paginationChanges$ = new BehaviorSubject({
paginationConfig: this.paginationConfig,
sortConfig: this.sortConfig
});
const currentPagination$ = this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig);
const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig);
this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe(
switchMap(([currentPagination, currentSort]) => this.collectionRD$.pipe(
getFirstSucceededRemoteData(),
map((rd) => rd.payload.id),
switchMap((id: string) => {
return this.searchService.search<Item>(
new PaginatedSearchOptions({
scope: id,
pagination: currentPagination,
sort: currentSort,
dsoTypes: [DSpaceObjectType.ITEM]
}), null, true, true, ...BROWSE_LINKS_TO_FOLLOW)
.pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
}),
startWith(undefined) // Make sure switching pages shows loading component
)
)
);
this.collectionPageRoute$ = this.collectionRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((collection) => getCollectionPageRoute(collection.id))
@@ -133,9 +75,5 @@ export class CollectionPageComponent implements OnInit {
return isNotEmpty(object);
}
ngOnDestroy(): void {
this.paginationService.clearPagination(this.paginationConfig.id);
}
}

View File

@@ -18,6 +18,19 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen
import { ComcolModule } from '../shared/comcol/comcol.module';
import { DsoSharedModule } from '../dso-shared/dso-shared.module';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
import { BrowseByPageModule } from '../browse-by/browse-by-page.module';
import { CollectionRecentlyAddedComponent } from './sections/recently-added/collection-recently-added.component';
const DECLARATIONS = [
CollectionPageComponent,
ThemedCollectionPageComponent,
CreateCollectionPageComponent,
DeleteCollectionPageComponent,
EditItemTemplatePageComponent,
ThemedEditItemTemplatePageComponent,
CollectionItemMapperComponent,
CollectionRecentlyAddedComponent,
];
@NgModule({
imports: [
@@ -30,15 +43,10 @@ import { DsoPageModule } from '../shared/dso-page/dso-page.module';
ComcolModule,
DsoSharedModule,
DsoPageModule,
BrowseByPageModule,
],
declarations: [
CollectionPageComponent,
ThemedCollectionPageComponent,
CreateCollectionPageComponent,
DeleteCollectionPageComponent,
EditItemTemplatePageComponent,
ThemedEditItemTemplatePageComponent,
CollectionItemMapperComponent
...DECLARATIONS,
],
providers: [
SearchService,

View File

@@ -0,0 +1,18 @@
<ng-container *ngVar="(itemRD$ | async) as itemRD">
<div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn>
<h3 class="sr-only">{{'collection.page.browse.recent.head' | translate}}</h3>
<ds-viewable-collection
[config]="paginationConfig"
[sortConfig]="sortConfig"
[objects]="itemRD"
[hideGear]="true">
</ds-viewable-collection>
</div>
<ds-error *ngIf="itemRD?.hasFailed"
message="{{'error.recent-submissions' | translate}}"></ds-error>
<ds-themed-loading *ngIf="!itemRD || itemRD.isLoading"
message="{{'loading.recent-submissions' | translate}}"></ds-themed-loading>
<div *ngIf="!itemRD?.isLoading && itemRD?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
{{'collection.page.browse.recent.empty' | translate}}
</div>
</ng-container>

View File

@@ -0,0 +1,53 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CollectionRecentlyAddedComponent } from './collection-recently-added.component';
import { APP_CONFIG } from '../../../../config/app-config.interface';
import { environment } from '../../../../environments/environment.test';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { SearchServiceStub } from '../../../shared/testing/search-service.stub';
import { SearchService } from '../../../core/shared/search/search.service';
import { VarDirective } from '../../../shared/utils/var.directive';
import { TranslateModule } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
describe('CollectionRecentlyAddedComponent', () => {
let component: CollectionRecentlyAddedComponent;
let fixture: ComponentFixture<CollectionRecentlyAddedComponent>;
let activatedRoute: ActivatedRouteStub;
let paginationService: PaginationServiceStub;
let searchService: SearchServiceStub;
beforeEach(async () => {
activatedRoute = new ActivatedRouteStub();
paginationService = new PaginationServiceStub();
searchService = new SearchServiceStub();
await TestBed.configureTestingModule({
declarations: [
CollectionRecentlyAddedComponent,
VarDirective,
],
imports: [
TranslateModule.forRoot(),
],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: APP_CONFIG, useValue: environment },
{ provide: PaginationService, useValue: paginationService },
{ provide: SearchService, useValue: SearchServiceStub },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(CollectionRecentlyAddedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,82 @@
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { Item } from '../../../core/shared/item.model';
import { switchMap, map, startWith, take } from 'rxjs/operators';
import { getFirstSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators';
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { BROWSE_LINKS_TO_FOLLOW } from '../../../core/browse/browse.service';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SortOptions, SortDirection } from '../../../core/cache/models/sort-options.model';
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
import { SearchService } from '../../../core/shared/search/search.service';
import { Collection } from '../../../core/shared/collection.model';
import { ActivatedRoute, Data } from '@angular/router';
import { fadeIn } from '../../../shared/animations/fade';
@Component({
selector: 'ds-collection-recently-added',
templateUrl: './collection-recently-added.component.html',
styleUrls: ['./collection-recently-added.component.scss'],
animations: [fadeIn],
})
export class CollectionRecentlyAddedComponent implements OnInit, OnDestroy {
paginationConfig: PaginationComponentOptions;
sortConfig: SortOptions;
collectionRD$: Observable<RemoteData<Collection>>;
itemRD$: Observable<RemoteData<PaginatedList<Item>>>;
constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
protected paginationService: PaginationService,
protected route: ActivatedRoute,
protected searchService: SearchService,
) {
this.paginationConfig = Object.assign(new PaginationComponentOptions(), {
id: 'cp',
currentPage: 1,
pageSize: this.appConfig.browseBy.pageSize,
});
this.sortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
}
ngOnInit(): void {
this.collectionRD$ = this.route.data.pipe(
map((data: Data) => data.dso as RemoteData<Collection>),
take(1),
);
this.itemRD$ = observableCombineLatest([
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig),
this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig),
]).pipe(
switchMap(([currentPagination, currentSort]: [PaginationComponentOptions, SortOptions]) => this.collectionRD$.pipe(
getFirstSucceededRemoteData(),
map((rd: RemoteData<Collection>) => rd.payload.id),
switchMap((id: string) => this.searchService.search<Item>(
new PaginatedSearchOptions({
scope: id,
pagination: currentPagination,
sort: currentSort,
dsoTypes: [DSpaceObjectType.ITEM]
}), null, true, true, ...BROWSE_LINKS_TO_FOLLOW).pipe(
toDSpaceObjectListRD()
) as Observable<RemoteData<PaginatedList<Item>>>),
startWith(undefined), // Make sure switching pages shows loading component
)),
);
}
ngOnDestroy(): void {
this.paginationService.clearPagination(this.paginationConfig.id);
}
}

View File

@@ -15,6 +15,10 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedCommunityPageComponent } from './themed-community-page.component';
import { MenuItemType } from '../shared/menu/menu-item-type.model';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-com-col-section.component';
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
import { BrowseByGuard } from '../browse-by/browse-by-guard';
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
@NgModule({
imports: [
@@ -48,7 +52,23 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
{
path: '',
component: ThemedCommunityPageComponent,
children: [
{
path: '',
pathMatch: 'full',
component: SubComColSectionComponent,
},
{
path: 'browse/:id',
pathMatch: 'full',
component: ComcolBrowseByComponent,
canActivate: [BrowseByGuard],
resolve: {
breadcrumb: BrowseByI18nBreadcrumbResolver,
},
data: { breadcrumbKey: 'browse.metadata' },
},
],
}
],
data: {

View File

@@ -28,8 +28,7 @@
<ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
</ds-themed-comcol-page-browse-by>
<ds-themed-community-page-sub-community-list [community]="communityPayload"></ds-themed-community-page-sub-community-list>
<ds-themed-community-page-sub-collection-list [community]="communityPayload"></ds-themed-community-page-sub-collection-list>
<router-outlet></router-outlet>
</section>
<footer *ngIf="communityPayload.copyrightText" class="border-top my-5 pt-4">
<!-- Copyright -->

View File

@@ -1,16 +1,10 @@
import { mergeMap, filter, map } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { CommunityDataService } from '../core/data/community-data.service';
import { RemoteData } from '../core/data/remote-data';
import { Bitstream } from '../core/shared/bitstream.model';
import { Community } from '../core/shared/community.model';
import { MetadataService } from '../core/metadata/metadata.service';
import { fadeInOut } from '../shared/animations/fade';
import { hasValue } from '../shared/empty.util';
import { getAllSucceededRemoteDataPayload} from '../core/shared/operators';
@@ -53,8 +47,6 @@ export class CommunityPageComponent implements OnInit {
communityPageRoute$: Observable<string>;
constructor(
private communityDataService: CommunityDataService,
private metadata: MetadataService,
private route: ActivatedRoute,
private router: Router,
private authService: AuthService,

View File

@@ -4,9 +4,9 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '../shared/shared.module';
import { CommunityPageComponent } from './community-page.component';
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
import { CommunityPageSubCollectionListComponent } from './sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component';
import { CommunityPageRoutingModule } from './community-page-routing.module';
import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component';
import { CommunityPageSubCommunityListComponent } from './sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component';
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
import { StatisticsModule } from '../statistics/statistics.module';
@@ -15,20 +15,25 @@ import { ThemedCommunityPageComponent } from './themed-community-page.component'
import { ComcolModule } from '../shared/comcol/comcol.module';
import {
ThemedCommunityPageSubCommunityListComponent
} from './sub-community-list/themed-community-page-sub-community-list.component';
} from './sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component';
import {
ThemedCollectionPageSubCollectionListComponent
} from './sub-collection-list/themed-community-page-sub-collection-list.component';
} from './sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-com-col-section.component';
import { BrowseByPageModule } from '../browse-by/browse-by-page.module';
const DECLARATIONS = [CommunityPageComponent,
const DECLARATIONS = [
CommunityPageComponent,
ThemedCommunityPageComponent,
ThemedCommunityPageSubCommunityListComponent,
CommunityPageSubCollectionListComponent,
ThemedCollectionPageSubCollectionListComponent,
CommunityPageSubCommunityListComponent,
CreateCommunityPageComponent,
DeleteCommunityPageComponent];
DeleteCommunityPageComponent,
SubComColSectionComponent,
];
@NgModule({
imports: [
@@ -39,6 +44,7 @@ const DECLARATIONS = [CommunityPageComponent,
CommunityFormModule,
ComcolModule,
DsoPageModule,
BrowseByPageModule,
],
declarations: [
...DECLARATIONS

View File

@@ -1,6 +1,6 @@
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
<div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn>
<h2>{{'community.sub-collection-list.head' | translate}}</h2>
<h3>{{'community.sub-collection-list.head' | translate}}</h3>
<ds-viewable-collection
[config]="config"
[sortConfig]="sortConfig"

View File

@@ -8,27 +8,27 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component';
import { Community } from '../../core/shared/community.model';
import { SharedModule } from '../../shared/shared.module';
import { CollectionDataService } from '../../core/data/collection-data.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { buildPaginatedList } from '../../core/data/paginated-list.model';
import { PageInfo } from '../../core/shared/page-info.model';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { PaginationService } from '../../core/pagination/pagination.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { LinkHeadService } from '../../core/services/link-head.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
import { Community } from '../../../../core/shared/community.model';
import { SharedModule } from '../../../../shared/shared.module';
import { CollectionDataService } from '../../../../core/data/collection-data.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { HostWindowService } from '../../../../shared/host-window.service';
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../../../shared/theme-support/theme.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../../core/data/find-list-options.model';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { LinkHeadService } from '../../../../core/services/link-head.service';
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
import { createPaginatedList } from '../../../../shared/testing/utils.test';
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub';
describe('CommunityPageSubCollectionList Component', () => {
let comp: CommunityPageSubCollectionListComponent;

View File

@@ -1,19 +1,17 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model';
import { Community } from '../../core/shared/community.model';
import { fadeIn } from '../../shared/animations/fade';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { CollectionDataService } from '../../core/data/collection-data.service';
import { PaginationService } from '../../core/pagination/pagination.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { Collection } from '../../../../core/shared/collection.model';
import { Community } from '../../../../core/shared/community.model';
import { fadeIn } from '../../../../shared/animations/fade';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model';
import { CollectionDataService } from '../../../../core/data/collection-data.service';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { switchMap } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util';
import { hasValue } from '../../../../shared/empty.util';
@Component({
selector: 'ds-community-page-sub-collection-list',

View File

@@ -1,12 +1,12 @@
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { ThemedComponent } from '../../../../shared/theme-support/themed.component';
import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component';
import { Component, Input } from '@angular/core';
import { Community } from '../../core/shared/community.model';
import { Community } from '../../../../core/shared/community.model';
@Component({
selector: 'ds-themed-community-page-sub-collection-list',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
templateUrl: '../../../../shared/theme-support/themed.component.html',
})
export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent<CommunityPageSubCollectionListComponent> {
@Input() community: Community;
@@ -18,7 +18,7 @@ export class ThemedCollectionPageSubCollectionListComponent extends ThemedCompon
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/community-page/sub-collection-list/community-page-sub-collection-list.component`);
return import(`../../../../../themes/${themeName}/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component`);
}
protected importUnthemedComponent(): Promise<any> {

View File

@@ -0,0 +1,8 @@
<ng-container *ngIf="(community$ | async) as community">
<ds-themed-community-page-sub-community-list
[community]="community">
</ds-themed-community-page-sub-community-list>
<ds-themed-community-page-sub-collection-list
[community]="community">
</ds-themed-community-page-sub-collection-list>
</ng-container>

View File

@@ -0,0 +1,32 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SubComColSectionComponent } from './sub-com-col-section.component';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
describe('SubComColSectionComponent', () => {
let component: SubComColSectionComponent;
let fixture: ComponentFixture<SubComColSectionComponent>;
let activatedRoute: ActivatedRouteStub;
beforeEach(async () => {
activatedRoute = new ActivatedRouteStub();
await TestBed.configureTestingModule({
declarations: [
SubComColSectionComponent,
],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
],
}).compileComponents();
fixture = TestBed.createComponent(SubComColSectionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,28 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { Community } from '../../../core/shared/community.model';
import { ActivatedRoute, Data } from '@angular/router';
import { map } from 'rxjs/operators';
@Component({
selector: 'ds-sub-com-col-section',
templateUrl: './sub-com-col-section.component.html',
styleUrls: ['./sub-com-col-section.component.scss'],
})
export class SubComColSectionComponent implements OnInit {
community$: Observable<Community>;
constructor(
private route: ActivatedRoute,
) {
}
ngOnInit(): void {
this.community$ = this.route.data.pipe(
map((data: Data) => (data.dso as RemoteData<Community>).payload),
);
}
}

View File

@@ -1,6 +1,6 @@
<ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD">
<div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn>
<h2>{{'community.sub-community-list.head' | translate}}</h2>
<h3>{{'community.sub-community-list.head' | translate}}</h3>
<ds-viewable-collection
[config]="config"
[sortConfig]="sortConfig"

View File

@@ -8,27 +8,27 @@ import { By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component';
import { Community } from '../../core/shared/community.model';
import { buildPaginatedList } from '../../core/data/paginated-list.model';
import { PageInfo } from '../../core/shared/page-info.model';
import { SharedModule } from '../../shared/shared.module';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { CommunityDataService } from '../../core/data/community-data.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { PaginationService } from '../../core/pagination/pagination.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { LinkHeadService } from '../../core/services/link-head.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { Community } from '../../../../core/shared/community.model';
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { SharedModule } from '../../../../shared/shared.module';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { HostWindowService } from '../../../../shared/host-window.service';
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
import { CommunityDataService } from '../../../../core/data/community-data.service';
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../../../shared/theme-support/theme.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../../../core/data/find-list-options.model';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { LinkHeadService } from '../../../../core/services/link-head.service';
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub';
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
import { createPaginatedList } from '../../../../shared/testing/utils.test';
describe('CommunityPageSubCommunityListComponent Component', () => {
let comp: CommunityPageSubCommunityListComponent;

View File

@@ -3,16 +3,16 @@ import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model';
import { fadeIn } from '../../shared/animations/fade';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { CommunityDataService } from '../../core/data/community-data.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { Community } from '../../../../core/shared/community.model';
import { fadeIn } from '../../../../shared/animations/fade';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model';
import { CommunityDataService } from '../../../../core/data/community-data.service';
import { switchMap } from 'rxjs/operators';
import { PaginationService } from '../../core/pagination/pagination.service';
import { hasValue } from '../../shared/empty.util';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { hasValue } from '../../../../shared/empty.util';
@Component({
selector: 'ds-community-page-sub-community-list',

View File

@@ -1,12 +1,12 @@
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { ThemedComponent } from '../../../../shared/theme-support/themed.component';
import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component';
import { Component, Input } from '@angular/core';
import { Community } from '../../core/shared/community.model';
import { Community } from '../../../../core/shared/community.model';
@Component({
selector: 'ds-themed-community-page-sub-community-list',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
templateUrl: '../../../../shared/theme-support/themed.component.html',
})
export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent<CommunityPageSubCommunityListComponent> {
@@ -19,7 +19,7 @@ export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponen
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/community-page/sub-community-list/community-page-sub-community-list.component`);
return import(`../../../../../themes/${themeName}/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component`);
}
protected importUnthemedComponent(): Promise<any> {

View File

@@ -0,0 +1,31 @@
import { PublicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver';
describe('PublicationClaimBreadcrumbResolver', () => {
describe('resolve', () => {
let resolver: PublicationClaimBreadcrumbResolver;
let publicationClaimBreadcrumbService: any;
const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a';
const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a';
const expectedId = 'openaire:6bee076d-4f2a-4555-a475-04a267769b2a';
let route;
beforeEach(() => {
route = {
paramMap: {
get: function (param) {
return this[param];
},
targetId: expectedId,
}
};
publicationClaimBreadcrumbService = {};
resolver = new PublicationClaimBreadcrumbResolver(publicationClaimBreadcrumbService);
});
it('should resolve the breadcrumb config', () => {
const resolvedConfig = resolver.resolve(route as any, {url: fullPath } as any);
const expectedConfig = { provider: publicationClaimBreadcrumbService, key: expectedKey };
expect(resolvedConfig).toEqual(expectedConfig);
});
});
});

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import {BreadcrumbConfig} from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service';
@Injectable({
providedIn: 'root'
})
export class PublicationClaimBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> {
constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) {
}
/**
* Method that resolve Publication Claim item into a breadcrumb
* The parameter are retrieved by the url since part of the Publication Claim route config
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns BreadcrumbConfig object
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
const targetId = route.paramMap.get('targetId').split(':')[1];
return { provider: this.breadcrumbService, key: targetId };
}
}

View File

@@ -0,0 +1,51 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
import { getTestScheduler } from 'jasmine-marbles';
import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { of } from 'rxjs';
describe('PublicationClaimBreadcrumbService', () => {
let service: PublicationClaimBreadcrumbService;
let dsoNameService: any = {
getName: (str) => str
};
let translateService: any = {
instant: (str) => str,
};
let dataService: any = {
findById: (str) => createSuccessfulRemoteDataObject$(str),
};
let authorizationService: any = {
isAuthorized: (str) => of(true),
};
let exampleKey;
const ADMIN_PUBLICATION_CLAIMS_PATH = 'admin/notifications/publication-claim';
const ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY = 'admin.notifications.publicationclaim.page.title';
function init() {
exampleKey = 'suggestion.suggestionFor.breadcrumb';
}
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule({}).compileComponents();
}));
beforeEach(() => {
service = new PublicationClaimBreadcrumbService(dataService,dsoNameService,translateService, authorizationService);
});
describe('getBreadcrumbs', () => {
it('should return a breadcrumb based on a string', () => {
const breadcrumbs = service.getBreadcrumbs(exampleKey);
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY, ADMIN_PUBLICATION_CLAIMS_PATH),
new Breadcrumb(exampleKey, undefined)]
});
});
});
});

View File

@@ -0,0 +1,46 @@
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
import { BreadcrumbsProviderService } from './breadcrumbsProviderService';
import { combineLatest, Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { ItemDataService } from '../data/item-data.service';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { map } from 'rxjs/operators';
import { DSONameService } from './dso-name.service';
import { TranslateService } from '@ngx-translate/core';
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
import { FeatureID } from '../data/feature-authorization/feature-id';
/**
* Service to calculate Publication claims breadcrumbs
*/
@Injectable({
providedIn: 'root'
})
export class PublicationClaimBreadcrumbService implements BreadcrumbsProviderService<string> {
private ADMIN_PUBLICATION_CLAIMS_PATH = 'admin/notifications/publication-claim';
private ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY = 'admin.notifications.publicationclaim.page.title';
constructor(private dataService: ItemDataService,
private dsoNameService: DSONameService,
private tranlsateService: TranslateService,
protected authorizationService: AuthorizationDataService) {
}
/**
* Method to calculate the breadcrumbs
* @param key The key used to resolve the breadcrumb
*/
getBreadcrumbs(key: string): Observable<Breadcrumb[]> {
return combineLatest([this.dataService.findById(key).pipe(getFirstCompletedRemoteData()),this.authorizationService.isAuthorized(FeatureID.AdministratorOf)]).pipe(
map(([item, isAdmin]) => {
const itemName = this.dsoNameService.getName(item.payload);
return isAdmin ? [new Breadcrumb(this.tranlsateService.instant(this.ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY), this.ADMIN_PUBLICATION_CLAIMS_PATH),
new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', {name: itemName}), undefined)] :
[new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', {name: itemName}), undefined)];
})
);
}
}

View File

@@ -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$);
}

View File

@@ -1,7 +1,7 @@
import { HALLink } from '../../shared/hal-link.model';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
import { getLinkDefinition, link } from './build-decorators';
import { dataService, getDataServiceFor, getLinkDefinition, link } from './build-decorators';
class TestHALResource implements HALResource {
_links: {
@@ -46,5 +46,17 @@ describe('build decorators', () => {
expect(result).toBeUndefined();
});
});
describe(`set data service`, () => {
it(`should throw error`, () => {
expect(dataService(null)).toThrow();
});
it(`should set properly data service for type`, () => {
const target = new TestHALResource();
dataService(testType)(target);
expect(getDataServiceFor(testType)).toEqual(target);
});
});
});
});

View File

@@ -3,13 +3,20 @@ import { hasNoValue, hasValue } from '../../../shared/empty.util';
import { GenericConstructor } from '../../shared/generic-constructor';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
import { getResourceTypeValueFor } from '../object-cache.reducer';
import {
getResourceTypeValueFor
} from '../object-cache.reducer';
import { InjectionToken } from '@angular/core';
import { CacheableObject } from '../cacheable-object.model';
import { TypedObject } from '../typed-object.model';
export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor<any>>('getDataServiceFor', {
providedIn: 'root',
factory: () => getDataServiceFor
});
export const LINK_DEFINITION_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>>('getLinkDefinition', {
providedIn: 'root',
factory: () => getLinkDefinition,
factory: () => getLinkDefinition
});
export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>>('getLinkDefinitions', {
providedIn: 'root',
@@ -20,6 +27,7 @@ const resolvedLinkKey = Symbol('resolvedLink');
const resolvedLinkMap = new Map();
const typeMap = new Map();
const dataServiceMap = new Map();
const linkMap = new Map();
/**
@@ -38,6 +46,39 @@ export function getClassForType(type: string | ResourceType) {
return typeMap.get(getResourceTypeValueFor(type));
}
/**
* A class decorator to indicate that this class is a dataservice
* for a given resource type.
*
* "dataservice" in this context means that it has findByHref and
* findAllByHref methods.
*
* @param resourceType the resource type the class is a dataservice for
*/
export function dataService(resourceType: ResourceType): any {
return (target: any) => {
if (hasNoValue(resourceType)) {
throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`);
}
const existingDataservice = dataServiceMap.get(resourceType.value);
if (hasValue(existingDataservice)) {
throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`);
}
dataServiceMap.set(resourceType.value, target);
};
}
/**
* Return the dataservice matching the given resource type
*
* @param resourceType the resource type you want the matching dataservice for
*/
export function getDataServiceFor<T extends CacheableObject>(resourceType: ResourceType) {
return dataServiceMap.get(resourceType.value);
}
/**
* A class to represent the data that can be set by the @link decorator
*/
@@ -65,7 +106,7 @@ export const link = <T extends HALResource>(
resourceType: ResourceType,
isList = false,
linkName?: keyof T['_links'],
) => {
) => {
return (target: T, propertyName: string) => {
let targetMap = linkMap.get(target.constructor);

View File

@@ -161,6 +161,7 @@ export class RemoteDataBuildService {
} else {
// in case the elements of the paginated list were already filled in, because they're UnCacheableObjects
paginatedList.page = paginatedList.page
.filter((obj: any) => obj != null)
.map((obj: any) => this.plainObjectToInstance<T>(obj))
.map((obj: any) =>
this.linkService.resolveLinks(obj, ...pageLink.linksToFollow)

View File

@@ -185,6 +185,8 @@ import { FlatBrowseDefinition } from './shared/flat-browse-definition.model';
import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model';
import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition';
import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model';
import { SuggestionTarget } from './suggestion-notifications/models/suggestion-target.model';
import { SuggestionSource } from './suggestion-notifications/models/suggestion-source.model';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -386,7 +388,9 @@ export const models =
IdentifierData,
Subscription,
ItemRequest,
BulkAccessConditionOptions
BulkAccessConditionOptions,
SuggestionTarget,
SuggestionSource
];
@NgModule({

View File

@@ -29,11 +29,11 @@ export const EMBED_SEPARATOR = '%2F';
/**
* Common functionality for data services.
* Specific functionality that not all services would need
* is implemented in "DataService feature" classes (e.g. {@link CreateData}
* is implemented in "UpdateDataServiceImpl feature" classes (e.g. {@link CreateData}
*
* All DataService (or DataService feature) classes must
* All UpdateDataServiceImpl (or UpdateDataServiceImpl feature) classes must
* - extend this class (or {@link IdentifiableDataService})
* - implement any DataService features it requires in order to forward calls to it
* - implement any UpdateDataServiceImpl features it requires in order to forward calls to it
*
* ```
* export class SomeDataService extends BaseDataService<Something> implements CreateData<Something>, SearchData<Something> {
@@ -385,7 +385,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
/**
* Return the links to traverse from the root of the api to the
* endpoint this DataService represents
* endpoint this UpdateDataServiceImpl represents
*
* e.g. if the api root links to 'foo', and the endpoint at 'foo'
* links to 'bar' the linkPath for the BarDataService would be

View File

@@ -37,7 +37,7 @@ export interface CreateData<T extends CacheableObject> {
}
/**
* A DataService feature to create objects.
* A UpdateDataServiceImpl feature to create objects.
*
* Concrete data services can use this feature by implementing {@link CreateData}
* and delegating its method to an inner instance of this class.

View File

@@ -42,7 +42,7 @@ export interface FindAllData<T extends CacheableObject> {
}
/**
* A DataService feature to list all objects.
* A UpdateDataServiceImpl feature to list all objects.
*
* Concrete data services can use this feature by implementing {@link FindAllData}
* and delegating its method to an inner instance of this class.

Some files were not shown because too many files have changed in this diff Show More