diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2680420a21..52f20470a3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/cypress/e2e/browse-by-author.cy.ts b/cypress/e2e/browse-by-author.cy.ts index 07c20ad7c91..32c470231d7 100644 --- a/cypress/e2e/browse-by-author.cy.ts +++ b/cypress/e2e/browse-by-author.cy.ts @@ -5,9 +5,9 @@ describe('Browse By Author', () => { cy.visit('/browse/author'); // Wait for to be visible - cy.get('ds-browse-by-metadata-page').should('be.visible'); + cy.get('ds-browse-by-metadata').should('be.visible'); // Analyze for accessibility - testA11y('ds-browse-by-metadata-page'); + testA11y('ds-browse-by-metadata'); }); }); diff --git a/cypress/e2e/browse-by-dateissued.cy.ts b/cypress/e2e/browse-by-dateissued.cy.ts index 4d22420227c..7966f1c82eb 100644 --- a/cypress/e2e/browse-by-dateissued.cy.ts +++ b/cypress/e2e/browse-by-dateissued.cy.ts @@ -5,9 +5,9 @@ describe('Browse By Date Issued', () => { cy.visit('/browse/dateissued'); // Wait for to be visible - cy.get('ds-browse-by-date-page').should('be.visible'); + cy.get('ds-browse-by-date').should('be.visible'); // Analyze for accessibility - testA11y('ds-browse-by-date-page'); + testA11y('ds-browse-by-date'); }); }); diff --git a/cypress/e2e/browse-by-subject.cy.ts b/cypress/e2e/browse-by-subject.cy.ts index 89b791f03c4..57ca88d1032 100644 --- a/cypress/e2e/browse-by-subject.cy.ts +++ b/cypress/e2e/browse-by-subject.cy.ts @@ -5,9 +5,9 @@ describe('Browse By Subject', () => { cy.visit('/browse/subject'); // Wait for to be visible - cy.get('ds-browse-by-metadata-page').should('be.visible'); + cy.get('ds-browse-by-metadata').should('be.visible'); // Analyze for accessibility - testA11y('ds-browse-by-metadata-page'); + testA11y('ds-browse-by-metadata'); }); }); diff --git a/cypress/e2e/browse-by-title.cy.ts b/cypress/e2e/browse-by-title.cy.ts index e4e027586a8..09195c30df2 100644 --- a/cypress/e2e/browse-by-title.cy.ts +++ b/cypress/e2e/browse-by-title.cy.ts @@ -5,9 +5,9 @@ describe('Browse By Title', () => { cy.visit('/browse/title'); // Wait for to be visible - cy.get('ds-browse-by-title-page').should('be.visible'); + cy.get('ds-browse-by-title').should('be.visible'); // Analyze for accessibility - testA11y('ds-browse-by-title-page'); + testA11y('ds-browse-by-title'); }); }); diff --git a/docker/cli.assetstore.yml b/docker/cli.assetstore.yml index 40e4974c7c7..31bc53f64d8 100644 --- a/docker/cli.assetstore.yml +++ b/docker/cli.assetstore.yml @@ -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 diff --git a/docker/cli.yml b/docker/cli.yml index 223ec356b9c..cc266b186f9 100644 --- a/docker/cli.yml +++ b/docker/cli.yml @@ -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: diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index edbb5b07598..07993e20c62 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -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: \ No newline at end of file diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index ea766600efa..e1577ec8375 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -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: diff --git a/src/app/access-control/bulk-access/bulk-access.component.html b/src/app/access-control/bulk-access/bulk-access.component.html index 382caf85f46..c164cc5c319 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.html +++ b/src/app/access-control/bulk-access/bulk-access.component.html @@ -1,4 +1,5 @@
+

{{ 'admin.access-control.bulk-access.title' | translate }}

diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.html b/src/app/admin/admin-import-batch-page/batch-import-page.component.html index 10924434369..c51ee7597c5 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.html +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.html @@ -1,5 +1,5 @@
- +

{{'admin.batch-import.page.header' | translate}}

{{'admin.batch-import.page.help' | translate}}

selected collection: {{getDspaceObjectName()}}  @@ -28,7 +28,7 @@

{{'admin.batch-import.page.toggle.help' | translate}} - + {{'admin.registries.bitstream-formats - + @@ -35,6 +35,7 @@

{{'admin.registries.bitstream-formats [checked]="isSelected(bitstreamFormat) | async" (change)="selectBitStreamFormat(bitstreamFormat, $event)" > + {{'admin.registries.bitstream-formats.select' | translate}}}

diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html index 35bffad185c..d3be5df08e6 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html @@ -2,7 +2,7 @@
{{'admin.registries.bitstream-formats.table.selected' | translate}} {{'admin.registries.bitstream-formats.table.id' | translate}} {{'admin.registries.bitstream-formats.table.name' | translate}} {{'admin.registries.bitstream-formats.table.mimetype' | translate}}{{bitstreamFormat.id}}
- + @@ -34,6 +34,7 @@ diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html index a4a4613565e..85d1e90692a 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html @@ -1,11 +1,11 @@
-

{{messagePrefix + '.create' | translate}}

+

{{messagePrefix + '.create' | translate}}

-

{{messagePrefix + '.edit' | translate}}

+

{{messagePrefix + '.edit' | translate}}

-

{{messagePrefix + '.create' | translate}}

+

{{messagePrefix + '.create' | translate}}

-

{{messagePrefix + '.edit' | translate}}

+

{{messagePrefix + '.edit' | translate}}

{{'admin.registries.metadata.schemas.table.selected' | translate}} {{'admin.registries.metadata.schemas.table.id' | translate}} {{'admin.registries.metadata.schemas.table.namespace' | translate}} {{'admin.registries.metadata.schemas.table.name' | translate}}{{schema.id}}
- + @@ -33,12 +33,11 @@

{{'admin.registries.schema.fields.head' | translate}}

- diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html index c4b737849b6..639c47f7f8c 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts index dab6694f368..0e41a20d847 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts @@ -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 implements OnInit { - @ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective; +export class ItemAdminSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnDestroy, OnInit { + @ViewChild(DynamicComponentLoaderDirective, { static: true }) dynamicComponentLoaderDirective: DynamicComponentLoaderDirective; @ViewChild('badges', { static: true }) badges: ElementRef; @ViewChild('buttons', { static: true }) buttons: ElementRef; + protected compRef: ComponentRef; + 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 = this.getComponent(); - const viewContainerRef = this.listableObjectDirective.viewContainerRef; + const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef; viewContainerRef.clear(); - const componentRef = viewContainerRef.createComponent( - componentFactory, - 0, - undefined, - [ - [this.badges.nativeElement], - [this.buttons.nativeElement] - ]); - (componentRef.instance as any).object = this.object; - (componentRef.instance as any).index = this.index; - (componentRef.instance as any).linkType = this.linkType; - (componentRef.instance as any).listID = this.listID; + this.compRef = viewContainerRef.createComponent( + component, { + index: 0, + injector: undefined, + projectableNodes: [ + [this.badges.nativeElement], + [this.buttons.nativeElement], + ], + }, + ); + this.compRef.setInput('object',this.object); + this.compRef.setInput('index', this.index); + this.compRef.setInput('linkType', this.linkType); + this.compRef.setInput('listID', this.listID); + } + + ngOnDestroy(): void { + if (hasValue(this.compRef)) { + this.compRef.destroy(); + this.compRef = undefined; + } } /** diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html index 87bae0c2613..c5c2a5331ad 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts index 8035c53547e..b02fa476ea2 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts @@ -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(), diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts index fd9d21e227d..401140fd82a 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts @@ -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 { +export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent 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; + protected compRef: ComponentRef; + 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>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); this.item$.pipe(take(1)).subscribe((item: Item) => { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item)); + const component: GenericConstructor = this.getComponent(item); - const viewContainerRef = this.listableObjectDirective.viewContainerRef; - viewContainerRef.clear(); + const viewContainerRef = this.dynamicComponentLoaderDirective.viewContainerRef; + viewContainerRef.clear(); - const componentRef = viewContainerRef.createComponent( - componentFactory, - 0, - undefined, - [ + this.compRef = viewContainerRef.createComponent( + component, { + index: 0, + injector: undefined, + projectableNodes: [ [this.badges.nativeElement], - [this.buttons.nativeElement] - ]); - (componentRef.instance as any).object = item; - (componentRef.instance as any).index = this.index; - (componentRef.instance as any).linkType = this.linkType; - (componentRef.instance as any).listID = this.listID; - componentRef.changeDetectorRef.detectChanges(); - } - ); + [this.buttons.nativeElement], + ], + }, + ); + this.compRef.setInput('object', item); + this.compRef.setInput('index', this.index); + this.compRef.setInput('linkType', this.linkType); + this.compRef.setInput('listID', this.listID); + this.compRef.changeDetectorRef.detectChanges(); + }); + } + + ngOnDestroy(): void { + if (hasValue(this.compRef)) { + this.compRef.destroy(); + this.compRef = undefined; + } } /** diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.html b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.html index 767ad79786f..f78a0a3ca43 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.html +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.html @@ -1,4 +1,4 @@ - +
diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.spec.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.spec.ts index b9e752c1047..d023e57709f 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.spec.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.spec.ts @@ -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(), diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts index d6f39e79feb..0fe36056a9a 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts @@ -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 implements OnInit { +export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent 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; + 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>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); this.item$.pipe(take(1)).subscribe((item: Item) => { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item)); + const component: GenericConstructor = 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} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index deb68f1ea92..1a0dc423b9c 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -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'; @@ -40,6 +40,7 @@ import { import { ServerCheckGuard } from './core/server-check/server-check.guard'; 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 +95,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, diff --git a/src/app/browse-by/browse-by-date-page/themed-browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/themed-browse-by-date-page.component.ts deleted file mode 100644 index 8eeae0c5de5..00000000000 --- a/src/app/browse-by/browse-by-date-page/themed-browse-by-date-page.component.ts +++ /dev/null @@ -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 { - protected getComponentName(): string { - return 'BrowseByDatePageComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-date-page/browse-by-date-page.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-date-page.component`); - } -} diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts b/src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts similarity index 88% rename from src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts rename to src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts index e41d3a45b25..c5ef9020f1d 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts +++ b/src/app/browse-by/browse-by-date/browse-by-date.component.spec.ts @@ -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; +describe('BrowseByDateComponent', () => { + let comp: BrowseByDateComponent; + let fixture: ComponentFixture; 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(); }); }); diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date/browse-by-date.component.ts similarity index 68% rename from src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts rename to src/app/browse-by/browse-by-date/browse-by-date.component.ts index c52731a421f..3485519929b 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/browse-by/browse-by-date/browse-by-date.component.ts @@ -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, - protected browseService: BrowseService, - protected dsoService: DSpaceObjectDataService, - protected router: Router, - protected paginationService: PaginationService, - protected cdRef: ChangeDetectorRef, - @Inject(APP_CONFIG) public appConfig: AppConfig, - public dsoNameService: DSONameService, + 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, + 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> = this.browseService.getFirstItemFor(definition, scope, SortDirection.ASC); + const lastItemRD$: Observable> = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC); + this.loading$ = observableCombineLatest([ + firstItemRD$, + lastItemRD$, + ]).pipe( + map(([firstItemRD, lastItemRD]: [RemoteData, RemoteData]) => 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, RemoteData]) => { + 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; diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts index aac5ba27233..c3840470c6d 100644 --- a/src/app/browse-by/browse-by-guard.spec.ts +++ b/src/app/browse-by/browse-by-guard.spec.ts @@ -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, }, diff --git a/src/app/browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts index dd87699a84a..cca9a6a6752 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -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,11 +16,10 @@ import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths'; */ export class BrowseByGuard implements CanActivate { - constructor(protected dsoService: DSpaceObjectDataService, - protected translate: TranslateService, - protected browseDefinitionService: BrowseDefinitionDataService, - protected dsoNameService: DSONameService, - protected router: Router, + constructor( + protected translate: TranslateService, + protected browseDefinitionService: BrowseDefinitionDataService, + 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 = 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); - return observableOf(true); - } + 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, }); } } diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html deleted file mode 100644 index cfc2cbe3056..00000000000 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html +++ /dev/null @@ -1,67 +0,0 @@ -
- - -
- -
- - - - - - - - - - - - - - - -
- -
- - -
- -
- -
- - -
-
- - - - -
-
-
-
-
diff --git a/src/app/browse-by/browse-by-metadata-page/themed-browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata-page/themed-browse-by-metadata-page.component.ts deleted file mode 100644 index b0679258e91..00000000000 --- a/src/app/browse-by/browse-by-metadata-page/themed-browse-by-metadata-page.component.ts +++ /dev/null @@ -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 { - protected getComponentName(): string { - return 'BrowseByMetadataPageComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-metadata-page.component`); - } -} diff --git a/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html new file mode 100644 index 00000000000..52ef06206f0 --- /dev/null +++ b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html @@ -0,0 +1,21 @@ +
+ +
diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.scss similarity index 100% rename from src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss rename to src/app/browse-by/browse-by-metadata/browse-by-metadata.component.scss diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.spec.ts similarity index 91% rename from src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts rename to src/app/browse-by/browse-by-metadata/browse-by-metadata.component.spec.ts index a5beeb8a452..9fef2b1a35a 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts +++ b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.spec.ts @@ -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; +describe('BrowseByMetadataComponent', () => { + let comp: BrowseByMetadataComponent; + let fixture: ComponentFixture; 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', () => { diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts similarity index 76% rename from src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts rename to src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts index 75dbfcfc82a..00cfedba88a 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts @@ -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,27 +34,40 @@ 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 list of browse-entries to display + * The optional context */ - browseEntries$: Observable>>; + @Input() context: Context; /** - * The list of items to display when a value is present + * The {@link BrowseByDataType} of this Component */ - items$: Observable>>; + @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 = new BehaviorSubject(undefined); /** - * The current Community or Collection we're browsing metadata/items in + * The list of browse-entries to display */ - parent$: Observable>; + browseEntries$: Observable>>; /** - * The logo of the current Community or Collection + * The list of items to display when a value is present */ - logo$: Observable>; + items$: Observable>>; /** * 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,12 +162,12 @@ 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]) => { - this.browseId = params.id || this.defaultBrowseId; + ).subscribe(([params, scope, currentPage, currentSort]: [Params, string, PaginationComponentOptions, SortOptions]) => { + this.browseId = params.id || this.defaultBrowseId; this.authority = params.authority; if (typeof params.value === 'string'){ @@ -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>) => browseEntriesRD.isLoading), + ); this.items$ = undefined; } @@ -214,37 +233,9 @@ 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[]).pipe( - getFirstSucceededRemoteData() - ); - } - } - - /** - * Update the parent Community or Collection logo - */ - updateLogo() { - if (hasValue(this.parent$)) { - this.logo$ = this.parent$.pipe( - map((rd: RemoteData) => rd.payload), - filter((collectionOrCommunity: Collection | Community) => hasValue(collectionOrCommunity.logo)), - mergeMap((collectionOrCommunity: Collection | Community) => collectionOrCommunity.logo) - ); - } + this.loading$ = this.items$.pipe( + map((itemsRD: RemoteData>) => itemsRD.isLoading), + ); } /** @@ -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 ); } diff --git a/src/app/browse-by/browse-by-page.module.ts b/src/app/browse-by/browse-by-page.module.ts index 554a6c4f466..8a010f71056 100644 --- a/src/app/browse-by/browse-by-page.module.ts +++ b/src/app/browse-by/browse-by-page.module.ts @@ -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 { diff --git a/src/app/browse-by/browse-by-page/browse-by-page.component.html b/src/app/browse-by/browse-by-page/browse-by-page.component.html new file mode 100644 index 00000000000..6e9476e1e92 --- /dev/null +++ b/src/app/browse-by/browse-by-page/browse-by-page.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss b/src/app/browse-by/browse-by-page/browse-by-page.component.scss similarity index 100% rename from src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss rename to src/app/browse-by/browse-by-page/browse-by-page.component.scss diff --git a/src/app/browse-by/browse-by-page/browse-by-page.component.spec.ts b/src/app/browse-by/browse-by-page/browse-by-page.component.spec.ts new file mode 100644 index 00000000000..25483028ebd --- /dev/null +++ b/src/app/browse-by/browse-by-page/browse-by-page.component.spec.ts @@ -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: '', +}) +class BrowseByTestComponent { +} + +class TestBrowseByPageBrowseDefinition extends BrowseDefinition { + getRenderType(): BrowseByDataType { + return 'BrowseByPageComponent' as BrowseByDataType; + } +} + +describe('BrowseByPageComponent', () => { + let component: BrowseByPageComponent; + let fixture: ComponentFixture; + + 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(); + }); +}); diff --git a/src/app/browse-by/browse-by-page/browse-by-page.component.ts b/src/app/browse-by/browse-by-page/browse-by-page.component.ts new file mode 100644 index 00000000000..9df02562c60 --- /dev/null +++ b/src/app/browse-by/browse-by-page/browse-by-page.component.ts @@ -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; + + 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()), + ); + } + +} diff --git a/src/app/browse-by/browse-by-routing.module.ts b/src/app/browse-by/browse-by-routing.module.ts index bb67dc65aed..f07df26b32b 100644 --- a/src/app/browse-by/browse-by-routing.module.ts +++ b/src/app/browse-by/browse-by-routing.module.ts @@ -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' } diff --git a/src/app/browse-by/browse-by-switcher/browse-by-data-type.ts b/src/app/browse-by/browse-by-switcher/browse-by-data-type.ts new file mode 100644 index 00000000000..5324018b346 --- /dev/null +++ b/src/app/browse-by/browse-by-switcher/browse-by-data-type.ts @@ -0,0 +1,6 @@ +export enum BrowseByDataType { + Title = 'title', + Metadata = 'text', + Date = 'date', + Hierarchy = 'hierarchy', +} diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts index 19a6277151c..64604cdc04a 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts @@ -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); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts index b59a46cae11..62e666227d4 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts @@ -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>('getComponentByBrowseByType', { - providedIn: 'root', - factory: () => getComponentByBrowseByType -}); - -const map = new Map(); +const map: Map>>> = 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 { + let contextMap: Map>> = map.get(browseByType); + if (hasNoValue(contextMap)) { + contextMap = map.get(DEFAULT_BROWSE_BY_TYPE); + } + let themeMap: Map> = 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)) { diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.html b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.html deleted file mode 100644 index afe79cf2b10..00000000000 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts index c13405dd4d2..65e8c87c619 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts @@ -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: '', +}) +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], + void TestBed.configureTestingModule({ + declarations: [ + BrowseBySwitcherComponent, + DynamicComponentLoaderDirective, + ], providers: [ - { provide: ActivatedRoute, useValue: activatedRouteStub }, + BrowseByTestComponent, { provide: ThemeService, useValue: themeService }, - { provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) } ], - 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(); }); }); }); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts index 35e4edf9005..99b969417e6 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts @@ -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 { - - /** - * Resolved browse-by component - */ - browseByComponent: Observable; - - public constructor(protected route: ActivatedRoute, - protected themeService: ThemeService, - @Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor) { - } +export class BrowseBySwitcherComponent extends AbstractComponentLoaderComponent { + + @Input() context: Context; + + @Input() browseByType: BrowseByDataType; + + @Input() displayTitle: boolean; + + @Input() scope: string; + + protected inputNamesDependentForComponent: (keyof this & string)[] = [ + 'context', + 'browseByType', + ]; + + protected inputNames: (keyof this & string)[] = [ + 'context', + 'browseByType', + 'displayTitle', + 'scope', + ]; - /** - * 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())) - ); + public getComponent(): GenericConstructor { + return getComponentByBrowseByType(this.browseByType, this.context, this.themeService.getThemeName()); } } diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive.ts b/src/app/browse-by/browse-by-switcher/dynamic-component-loader.directive.ts similarity index 63% rename from src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive.ts rename to src/app/browse-by/browse-by-switcher/dynamic-component-loader.directive.ts index e569f6cc6f8..8c77df1cdb8 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive.ts +++ b/src/app/browse-by/browse-by-switcher/dynamic-component-loader.directive.ts @@ -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, diff --git a/src/app/browse-by/browse-by-switcher/themed-browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/themed-browse-by-switcher.component.ts deleted file mode 100644 index 0187d4e3c5e..00000000000 --- a/src/app/browse-by/browse-by-switcher/themed-browse-by-switcher.component.ts +++ /dev/null @@ -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 { - protected getComponentName(): string { - return 'BrowseBySwitcherComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-switcher/browse-by-switcher.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-switcher.component`); - } - - -} diff --git a/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts b/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts deleted file mode 100644 index 212044b8539..00000000000 --- a/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts +++ /dev/null @@ -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{ - - protected getComponentName(): string { - return 'BrowseByTaxonomyPageComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-taxonomy-page.component`); - } -} diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.html similarity index 70% rename from src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html rename to src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.html index c24ca934032..3dd9b6b25a6 100644 --- a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html +++ b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.html @@ -1,5 +1,11 @@ -
-

{{ ('browse.taxonomy_' + vocabularyName + '.title') | translate }}

+
+

+ {{ ('browse.title') | translate:{ + field: 'browse.metadata.' + vocabularyName | translate, + startsWith: '', + value: '', + } }} +

{{ ('browse.taxonomy_' + vocabularyName + '.title') | translate }} [queryParams]="queryParams" [queryParamsHandling]="'merge'"> {{ 'browse.taxonomy.button' | translate }} -
+
diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.scss b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.scss similarity index 100% rename from src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.scss rename to src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.scss diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.spec.ts similarity index 68% rename from src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts rename to src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.spec.ts index c724017b1fd..ac8dbff5982 100644 --- a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts +++ b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.spec.ts @@ -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; +describe('BrowseByTaxonomyComponent', () => { + let component: BrowseByTaxonomyComponent; + let fixture: ComponentFixture; 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(() => { diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts similarity index 50% rename from src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts rename to src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts index cf6345bf394..d23a3dd15c2 100644 --- a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts +++ b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts @@ -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 = 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; + browseDefinition$: Observable; /** * 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) { + 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); } /** @@ -86,9 +116,9 @@ export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy { * @param detail VocabularyEntryDetail to be added */ onSelect(detail: VocabularyEntryDetail): void { - this.selectedItems.push(detail); - this.filterValues = this.selectedItems - .map((item: VocabularyEntryDetail) => `${item.value},equals`); + this.selectedItems.push(detail); + this.filterValues = this.selectedItems + .map((item: VocabularyEntryDetail) => `${item.value},equals`); this.updateQueryParams(); } @@ -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()); } } diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts deleted file mode 100644 index 58df79ebe85..00000000000 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts +++ /dev/null @@ -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()); - } - -} diff --git a/src/app/browse-by/browse-by-title-page/themed-browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/themed-browse-by-title-page.component.ts deleted file mode 100644 index 4a1bcc0bc11..00000000000 --- a/src/app/browse-by/browse-by-title-page/themed-browse-by-title-page.component.ts +++ /dev/null @@ -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 { - protected getComponentName(): string { - return 'BrowseByTitlePageComponent'; - } - - protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/browse-by/browse-by-title-page/browse-by-title-page.component`); - } - - protected importUnthemedComponent(): Promise { - return import(`./browse-by-title-page.component`); - } -} diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts b/src/app/browse-by/browse-by-title/browse-by-title.component.spec.ts similarity index 87% rename from src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts rename to src/app/browse-by/browse-by-title/browse-by-title.component.spec.ts index e32c0ac430d..54394087ec3 100644 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts +++ b/src/app/browse-by/browse-by-title/browse-by-title.component.spec.ts @@ -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; +describe('BrowseByTitleComponent', () => { + let comp: BrowseByTitleComponent; + let fixture: ComponentFixture; 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; diff --git a/src/app/browse-by/browse-by-title/browse-by-title.component.ts b/src/app/browse-by/browse-by-title/browse-by-title.component.ts new file mode 100644 index 00000000000..7b603af48bc --- /dev/null +++ b/src/app/browse-by/browse-by-title/browse-by-title.component.ts @@ -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(); + } + +} diff --git a/src/app/browse-by/browse-by.module.ts b/src/app/browse-by/browse-by.module.ts index ec9f22347f7..b1263595b31 100644 --- a/src/app/browse-by/browse-by.module.ts +++ b/src/app/browse-by/browse-by.module.ts @@ -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 { diff --git a/src/app/collection-page/collection-page-routing.module.ts b/src/app/collection-page/collection-page-routing.module.ts index 9dc25b778ee..5ddef6ca68b 100644 --- a/src/app/collection-page/collection-page-routing.module.ts +++ b/src/app/collection-page/collection-page-routing.module.ts @@ -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, - pathMatch: 'full', + children: [ + { + path: '', + pathMatch: 'full', + component: CollectionRecentlyAddedComponent, + }, + { + path: 'browse/:id', + pathMatch: 'full', + component: ComcolBrowseByComponent, + canActivate: [BrowseByGuard], + resolve: { + breadcrumb: BrowseByI18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'browse.metadata' }, + }, + ], } ], data: { diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html index 3cbd08f1325..21cc94af68d 100644 --- a/src/app/collection-page/collection-page.component.html +++ b/src/app/collection-page/collection-page.component.html @@ -1,78 +1,61 @@
-
-
-
- -
-
- - - - - - +
+
+
+ +
+
+ + + + + + - - - - - - - - - -
- -
-
- - - + + + + + + + + + +
+ +
+
+ + + - -
-

{{'collection.page.browse.recent.head' | translate}}

- - -
- - - -
-
-
+ + +
+ [content]="collection.copyrightText" + [hasInnerHtml]="true">
- - + +
diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts index 16704cef52e..6b3cbbe64eb 100644 --- a/src/app/collection-page/collection-page.component.ts +++ b/src/app/collection-page/collection-page.component.ts @@ -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>; - itemRD$: Observable>>; logoRD$: Observable>; 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; 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( - new PaginatedSearchOptions({ - scope: id, - pagination: currentPagination, - sort: currentSort, - dsoTypes: [DSpaceObjectType.ITEM] - }), null, true, true, ...BROWSE_LINKS_TO_FOLLOW) - .pipe(toDSpaceObjectListRD()) as Observable>>; - }), - 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); - } - } diff --git a/src/app/collection-page/collection-page.module.ts b/src/app/collection-page/collection-page.module.ts index 6bcefed2b72..8782be0a45f 100644 --- a/src/app/collection-page/collection-page.module.ts +++ b/src/app/collection-page/collection-page.module.ts @@ -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, diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html index ffd8f713436..1cf40159ec9 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html @@ -1,17 +1,17 @@
- + {{ 'collection.edit.template.label' | translate}}
diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.html b/src/app/collection-page/sections/recently-added/collection-recently-added.component.html new file mode 100644 index 00000000000..002b8cceda7 --- /dev/null +++ b/src/app/collection-page/sections/recently-added/collection-recently-added.component.html @@ -0,0 +1,18 @@ + +
+

{{'collection.page.browse.recent.head' | translate}}

+ + +
+ + + +
diff --git a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.scss b/src/app/collection-page/sections/recently-added/collection-recently-added.component.scss similarity index 100% rename from src/app/community-page/sub-community-list/community-page-sub-community-list.component.scss rename to src/app/collection-page/sections/recently-added/collection-recently-added.component.scss diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.spec.ts b/src/app/collection-page/sections/recently-added/collection-recently-added.component.spec.ts new file mode 100644 index 00000000000..4acc24e3f59 --- /dev/null +++ b/src/app/collection-page/sections/recently-added/collection-recently-added.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/src/app/collection-page/sections/recently-added/collection-recently-added.component.ts b/src/app/collection-page/sections/recently-added/collection-recently-added.component.ts new file mode 100644 index 00000000000..65af77a63b5 --- /dev/null +++ b/src/app/collection-page/sections/recently-added/collection-recently-added.component.ts @@ -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>; + + itemRD$: Observable>>; + + 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), + 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) => rd.payload.id), + switchMap((id: string) => this.searchService.search( + new PaginatedSearchOptions({ + scope: id, + pagination: currentPagination, + sort: currentSort, + dsoTypes: [DSpaceObjectType.ITEM] + }), null, true, true, ...BROWSE_LINKS_TO_FOLLOW).pipe( + toDSpaceObjectListRD() + ) as Observable>>), + startWith(undefined), // Make sure switching pages shows loading component + )), + ); + } + + ngOnDestroy(): void { + this.paginationService.clearPagination(this.paginationConfig.id); + } + +} diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index eed9ca64cad..7ccf24a7619 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -4,9 +4,9 @@
- +
+ +
{{ dsoNameService.getName(node.payload) }} @@ -44,10 +47,9 @@
- + {{node.payload.shortDescription}} @@ -56,10 +58,9 @@
- +
@@ -67,9 +68,9 @@
- +
{{ dsoNameService.getName(node.payload) }}
@@ -77,10 +78,9 @@
- + {{node.payload.shortDescription}} diff --git a/src/app/community-list-page/community-list/community-list.component.scss b/src/app/community-list-page/community-list/community-list.component.scss new file mode 100644 index 00000000000..2e33380a29f --- /dev/null +++ b/src/app/community-list-page/community-list/community-list.component.scss @@ -0,0 +1,4 @@ +::ng-deep .fa-chevron-right::before { + display: block; + width: 16px; +} diff --git a/src/app/community-list-page/community-list/community-list.component.spec.ts b/src/app/community-list-page/community-list/community-list.component.spec.ts index fb47f4994d2..cec1b555ab8 100644 --- a/src/app/community-list-page/community-list/community-list.component.spec.ts +++ b/src/app/community-list-page/community-list/community-list.component.spec.ts @@ -5,7 +5,7 @@ import { CommunityListService, showMoreFlatNode, toFlatNode } from '../community import { CdkTreeModule } from '@angular/cdk/tree'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { RouterTestingModule } from '@angular/router/testing'; import { Community } from '../../core/shared/community.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -300,12 +300,14 @@ describe('CommunityListComponent', () => { describe('second top community node is expanded and has more children (collections) than page size of collection', () => { describe('children of second top com are added (page-limited pageSize 2)', () => { - let allNodes; + let allNodes: DebugElement[]; beforeEach(fakeAsync(() => { - const chevronExpand = fixture.debugElement.queryAll(By.css('.expandable-node button')); - const chevronExpandSpan = fixture.debugElement.queryAll(By.css('.expandable-node button span')); - if (chevronExpandSpan[1].nativeElement.classList.contains('fa-chevron-right')) { - chevronExpand[1].nativeElement.click(); + const toggleButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.expandable-node button')); + const toggleButtonText: DebugElement = toggleButtons[1].query(By.css('span')); + expect(toggleButtonText).not.toBeNull(); + + if (toggleButtonText.nativeElement.classList.contains('fa-chevron-right')) { + toggleButtons[1].nativeElement.click(); tick(); fixture.detectChanges(); } @@ -315,17 +317,18 @@ describe('CommunityListComponent', () => { allNodes = [...expandableNodesFound, ...childlessNodesFound]; })); it('tree contains 2 (page-limited) top com, 2 (page-limited) coll of 2nd top com, a show more for those page-limited coll and show more for page-limited top com', () => { - mockTopFlatnodesUnexpanded.slice(0, 2).map((topFlatnode: FlatNode) => { - expect(allNodes.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === topFlatnode.name); - })).toBeTruthy(); - }); - mockCollectionsPage1.map((coll) => { - expect(allNodes.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === coll.name); - })).toBeTruthy(); - }); + const allNodeNames: string[] = allNodes.map((node: DebugElement) => node.nativeElement.innerText.trim()); expect(allNodes.length).toEqual(4); + const flatNodes: string[] = mockTopFlatnodesUnexpanded.slice(0, 2).map((flatNode: FlatNode) => flatNode.name); + for (const flatNode of flatNodes) { + expect(allNodeNames).toContain(flatNode); + } + expect(flatNodes.length).toBe(2); + const page1CollectionNames: string[] = mockCollectionsPage1.map((collection: Collection) => collection.name); + for (const collectionName of page1CollectionNames) { + expect(allNodeNames).toContain(collectionName); + } + expect(page1CollectionNames.length).toBe(2); const showMoreEl = fixture.debugElement.queryAll(By.css('.show-more-node')); expect(showMoreEl.length).toEqual(2); }); diff --git a/src/app/community-list-page/community-list/community-list.component.ts b/src/app/community-list-page/community-list/community-list.component.ts index 6b5c6578e1f..031a4224692 100644 --- a/src/app/community-list-page/community-list/community-list.component.ts +++ b/src/app/community-list-page/community-list/community-list.component.ts @@ -19,6 +19,7 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-community-list', templateUrl: './community-list.component.html', + styleUrls: ['./community-list.component.scss'], }) export class CommunityListComponent implements OnInit, OnDestroy { diff --git a/src/app/community-page/community-page-routing.module.ts b/src/app/community-page/community-page-routing.module.ts index c37f8832f84..5ca544bb541 100644 --- a/src/app/community-page/community-page-routing.module.ts +++ b/src/app/community-page/community-page-routing.module.ts @@ -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, - pathMatch: 'full', + children: [ + { + path: '', + pathMatch: 'full', + component: SubComColSectionComponent, + }, + { + path: 'browse/:id', + pathMatch: 'full', + component: ComcolBrowseByComponent, + canActivate: [BrowseByGuard], + resolve: { + breadcrumb: BrowseByI18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'browse.metadata' }, + }, + ], } ], data: { diff --git a/src/app/community-page/community-page.component.html b/src/app/community-page/community-page.component.html index 671bf28fd1c..b3e577af7d3 100644 --- a/src/app/community-page/community-page.component.html +++ b/src/app/community-page/community-page.component.html @@ -7,7 +7,7 @@ - + @@ -17,7 +17,7 @@ + [title]="'community.page.news'"> @@ -28,10 +28,9 @@ - - + -
+
diff --git a/src/app/community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts index a5bbff3cee7..206aa54cb07 100644 --- a/src/app/community-page/community-page.component.ts +++ b/src/app/community-page/community-page.component.ts @@ -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; constructor( - private communityDataService: CommunityDataService, - private metadata: MetadataService, private route: ActivatedRoute, private router: Router, private authService: AuthService, diff --git a/src/app/community-page/community-page.module.ts b/src/app/community-page/community-page.module.ts index 45ffb2a7868..5ebd7c30572 100644 --- a/src/app/community-page/community-page.module.ts +++ b/src/app/community-page/community-page.module.ts @@ -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 diff --git a/src/app/community-page/delete-community-page/delete-community-page.component.html b/src/app/community-page/delete-community-page/delete-community-page.component.html index ef5fdade73b..1e71fb7f494 100644 --- a/src/app/community-page/delete-community-page/delete-community-page.component.html +++ b/src/app/community-page/delete-community-page/delete-community-page.component.html @@ -7,11 +7,11 @@

{{ 'community.delete.head' | translat
diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html similarity index 90% rename from src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html rename to src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html index 69f16ee3ac0..b5fbf1a01dd 100644 --- a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html @@ -1,6 +1,6 @@
-

{{'community.sub-collection-list.head' | translate}}

+

{{'community.sub-collection-list.head' | translate}}

{ let comp: CommunityPageSubCollectionListComponent; diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts similarity index 79% rename from src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts index ed14096ce03..92e689b127f 100644 --- a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts @@ -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', diff --git a/src/app/community-page/sub-collection-list/themed-community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts similarity index 68% rename from src/app/community-page/sub-collection-list/themed-community-page-sub-collection-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts index f1f49f204c2..ebbec33e8e9 100644 --- a/src/app/community-page/sub-collection-list/themed-community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts @@ -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 { @Input() community: Community; @@ -18,7 +18,7 @@ export class ThemedCollectionPageSubCollectionListComponent extends ThemedCompon } protected importThemedComponent(themeName: string): Promise { - 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 { diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html new file mode 100644 index 00000000000..515e08ffdfe --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.scss b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.scss similarity index 100% rename from src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.scss rename to src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.scss diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts new file mode 100644 index 00000000000..804299d3d98 --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts new file mode 100644 index 00000000000..ff30e51607b --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts @@ -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; + + constructor( + private route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.community$ = this.route.data.pipe( + map((data: Data) => (data.dso as RemoteData).payload), + ); + } + +} diff --git a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html similarity index 90% rename from src/app/community-page/sub-community-list/community-page-sub-community-list.component.html rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html index be2788a9f40..0834d08ba58 100644 --- a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html @@ -1,6 +1,6 @@
-

{{'community.sub-community-list.head' | translate}}

+

{{'community.sub-community-list.head' | translate}}

{ let comp: CommunityPageSubCommunityListComponent; diff --git a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts similarity index 81% rename from src/app/community-page/sub-community-list/community-page-sub-community-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts index 08c9509edbd..3108be8a601 100644 --- a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts @@ -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', diff --git a/src/app/community-page/sub-community-list/themed-community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts similarity index 68% rename from src/app/community-page/sub-community-list/themed-community-page-sub-community-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts index 852c53186ef..9c500cac10d 100644 --- a/src/app/community-page/sub-community-list/themed-community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts @@ -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 { @@ -19,7 +19,7 @@ export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponen } protected importThemedComponent(themeName: string): Promise { - 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 { diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index b210b349494..58bbc0b8700 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -105,7 +105,7 @@ export class BrowseService { }) ); if (options.fetchThumbnail ) { - return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); } return this.hrefOnlyDataService.findListByHref(href$); } @@ -153,7 +153,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail) { - return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); } return this.hrefOnlyDataService.findListByHref(href$); } diff --git a/src/app/core/data/base/base-data.service.spec.ts b/src/app/core/data/base/base-data.service.spec.ts index 6a18ae27e58..3366209179d 100644 --- a/src/app/core/data/base/base-data.service.spec.ts +++ b/src/app/core/data/base/base-data.service.spec.ts @@ -78,6 +78,8 @@ describe('BaseDataService', () => { const msToLive = 15 * 60 * 1000; const payload = { foo: 'bar', + followLink1: {}, + followLink2: {}, _links: { self: Object.assign(new HALLink(), { href: 'self-test-link', @@ -413,7 +415,7 @@ describe('BaseDataService', () => { expectObservable(service.findByHref(selfLink, false, false, ...linksToFollow)).toBe(expected, values); flush(); - expect(objectCache.addDependency).toHaveBeenCalledTimes(4); + expect(objectCache.addDependency).toHaveBeenCalledTimes(3); }); }); }); @@ -621,7 +623,7 @@ describe('BaseDataService', () => { expectObservable(service.findListByHref(selfLink, findListOptions, false, false, ...linksToFollow)).toBe(expected, values); flush(); - expect(objectCache.addDependency).toHaveBeenCalledTimes(4); + expect(objectCache.addDependency).toHaveBeenCalledTimes(3); }); }); }); diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index 747f172fcd5..e9b51aa2a15 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -282,12 +282,15 @@ export class BaseDataService implements HALDataServic // Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object tap((remoteDataObject: RemoteData) => { if (hasValue(remoteDataObject?.payload?._links)) { - for (const followLink of Object.values(remoteDataObject.payload._links)) { - // followLink can be either an individual HALLink or a HALLink[] - const followLinksList: HALLink[] = [].concat(followLink); - for (const individualFollowLink of followLinksList) { - if (hasValue(individualFollowLink?.href)) { - this.addDependency(response$, individualFollowLink.href); + for (const followLinkName of Object.keys(remoteDataObject.payload._links)) { + // only add the followLinks if they are embedded + if (hasValue(remoteDataObject.payload[followLinkName]) && followLinkName !== 'self') { + // followLink can be either an individual HALLink or a HALLink[] + const followLinksList: HALLink[] = [].concat(remoteDataObject.payload._links[followLinkName]); + for (const individualFollowLink of followLinksList) { + if (hasValue(individualFollowLink?.href)) { + this.addDependency(response$, individualFollowLink.href); + } } } } @@ -334,12 +337,15 @@ export class BaseDataService implements HALDataServic if (hasValue(remoteDataObject?.payload?.page)) { for (const object of remoteDataObject.payload.page) { if (hasValue(object?._links)) { - for (const followLink of Object.values(object._links)) { - // followLink can be either an individual HALLink or a HALLink[] - const followLinksList: HALLink[] = [].concat(followLink); - for (const individualFollowLink of followLinksList) { - if (hasValue(individualFollowLink?.href)) { - this.addDependency(response$, individualFollowLink.href); + for (const followLinkName of Object.keys(object._links)) { + // only add the followLinks if they are embedded + if (hasValue(object[followLinkName]) && followLinkName !== 'self') { + // followLink can be either an individual HALLink or a HALLink[] + const followLinksList: HALLink[] = [].concat(object._links[followLinkName]); + for (const individualFollowLink of followLinksList) { + if (hasValue(individualFollowLink?.href)) { + this.addDependency(response$, individualFollowLink.href); + } } } } diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index e2943f1762f..b5f0c60dc6b 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -34,5 +34,6 @@ export enum FeatureID { CanEditItem = 'canEditItem', CanRegisterDOI = 'canRegisterDOI', CanSubscribe = 'canSubscribeDso', - CanSeeQA = 'canSeeQA' + EPersonForgotPassword = 'epersonForgotPassword', + CanSeeQA = 'canSeeQA', } diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index 50a77c63a4b..080a4a4c09f 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -245,12 +245,14 @@ export class ProcessDataService extends IdentifiableDataService impleme ) ).subscribe((processRD: RemoteData) => { this.clearCurrentTimeout(processId); - const nextTimeout = this.timer(() => { - this.activelyBeingPolled.delete(processId); - this.invalidateByHref(processRD.payload._links.self.href); - }, pollingIntervalInMs); + if (processRD.hasSucceeded) { + const nextTimeout = this.timer(() => { + this.activelyBeingPolled.delete(processId); + this.invalidateByHref(processRD.payload._links.self.href); + }, pollingIntervalInMs); - this.activelyBeingPolled.set(processId, nextTimeout); + this.activelyBeingPolled.set(processId, nextTimeout); + } }); this.subs.set(processId, sub); diff --git a/src/app/core/json-patch/json-patch-operations.reducer.ts b/src/app/core/json-patch/json-patch-operations.reducer.ts index 5e00027edb9..86edd339e8f 100644 --- a/src/app/core/json-patch/json-patch-operations.reducer.ts +++ b/src/app/core/json-patch/json-patch-operations.reducer.ts @@ -351,7 +351,28 @@ function addOperationToList(body: JsonPatchOperationObject[], actionType, target newBody.push(makeOperationEntry({ op: JsonPatchOperationType.move, from: fromPath, path: targetPath })); break; } - return newBody; + return dedupeOperationEntries(newBody); +} + +/** + * Dedupe operation entries by op and path. This prevents processing unnecessary patches in a single PATCH request. + * + * @param body JSON patch operation object entries + * @returns deduped JSON patch operation object entries + */ +function dedupeOperationEntries(body: JsonPatchOperationObject[]): JsonPatchOperationObject[] { + const ops = new Map(); + for (let i = body.length - 1; i >= 0; i--) { + const patch = body[i].operation; + const key = `${patch.op}-${patch.path}`; + if (!ops.has(key)) { + ops.set(key, patch); + } else { + body.splice(i, 1); + } + } + + return body; } function makeOperationEntry(operation) { diff --git a/src/app/core/rest-property/forgot-password-check-guard.guard.ts b/src/app/core/rest-property/forgot-password-check-guard.guard.ts new file mode 100644 index 00000000000..438a532c7b9 --- /dev/null +++ b/src/app/core/rest-property/forgot-password-check-guard.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../data/feature-authorization/feature-id'; +import { + SingleFeatureAuthorizationGuard +} from '../data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard'; +import { AuthService } from '../auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard that checks if the forgot-password feature is enabled + */ +export class ForgotPasswordCheckGuard extends SingleFeatureAuthorizationGuard { + + constructor( + protected readonly authorizationService: AuthorizationDataService, + protected readonly router: Router, + protected readonly authService: AuthService + ) { + super(authorizationService, router, authService); + } + + getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return of(FeatureID.EPersonForgotPassword); + } + +} diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index a5bed53c9fd..72239b26f75 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -1,5 +1,6 @@ import { autoserialize } from 'cerialize'; import { CacheableObject } from '../cache/cacheable-object.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; /** * Base class for BrowseDefinition models @@ -12,5 +13,5 @@ export abstract class BrowseDefinition extends CacheableObject { /** * Get the render type of the BrowseDefinition model */ - abstract getRenderType(): string; + abstract getRenderType(): BrowseByDataType; } diff --git a/src/app/core/shared/flat-browse-definition.model.ts b/src/app/core/shared/flat-browse-definition.model.ts index 086fca891bb..9f37f1c422c 100644 --- a/src/app/core/shared/flat-browse-definition.model.ts +++ b/src/app/core/shared/flat-browse-definition.model.ts @@ -5,6 +5,7 @@ import { FLAT_BROWSE_DEFINITION } from './flat-browse-definition.resource-type'; import { ResourceType } from './resource-type'; import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition'; import { HALLink } from './hal-link.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; /** * BrowseDefinition model for browses of type 'flatBrowse' @@ -30,7 +31,7 @@ export class FlatBrowseDefinition extends NonHierarchicalBrowseDefinition { items: HALLink; }; - getRenderType(): string { + getRenderType(): BrowseByDataType { return this.dataType; } } diff --git a/src/app/core/shared/hierarchical-browse-definition.model.ts b/src/app/core/shared/hierarchical-browse-definition.model.ts index d561fff643f..2410bf7b7a4 100644 --- a/src/app/core/shared/hierarchical-browse-definition.model.ts +++ b/src/app/core/shared/hierarchical-browse-definition.model.ts @@ -5,6 +5,7 @@ import { HIERARCHICAL_BROWSE_DEFINITION } from './hierarchical-browse-definition import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; import { BrowseDefinition } from './browse-definition.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; /** * BrowseDefinition model for browses of type 'hierarchicalBrowse' @@ -39,7 +40,7 @@ export class HierarchicalBrowseDefinition extends BrowseDefinition { vocabulary: HALLink; }; - getRenderType(): string { - return 'hierarchy'; + getRenderType(): BrowseByDataType { + return BrowseByDataType.Hierarchy; } } diff --git a/src/app/core/shared/non-hierarchical-browse-definition.ts b/src/app/core/shared/non-hierarchical-browse-definition.ts index d5481fcc8d0..e3319affdb9 100644 --- a/src/app/core/shared/non-hierarchical-browse-definition.ts +++ b/src/app/core/shared/non-hierarchical-browse-definition.ts @@ -1,6 +1,6 @@ import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { SortOption } from './sort-option.model'; -import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; import { BrowseDefinition } from './browse-definition.model'; /** diff --git a/src/app/core/shared/value-list-browse-definition.model.ts b/src/app/core/shared/value-list-browse-definition.model.ts index 3378263962e..0302ec59c73 100644 --- a/src/app/core/shared/value-list-browse-definition.model.ts +++ b/src/app/core/shared/value-list-browse-definition.model.ts @@ -5,6 +5,7 @@ import { VALUE_LIST_BROWSE_DEFINITION } from './value-list-browse-definition.res import { ResourceType } from './resource-type'; import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition'; import { HALLink } from './hal-link.model'; +import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-data-type'; /** * BrowseDefinition model for browses of type 'valueList' @@ -30,7 +31,7 @@ export class ValueListBrowseDefinition extends NonHierarchicalBrowseDefinition { entries: HALLink; }; - getRenderType(): string { + getRenderType(): BrowseByDataType { return this.dataType; } } diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html index 24c3dc5cd7a..8fb676a7247 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -2,23 +2,23 @@
@@ -74,16 +74,19 @@
diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html index 4c310bd81bd..3f5af76051e 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html @@ -1,6 +1,7 @@ diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html index c4e31d3d81d..58f39b8d0b8 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html @@ -11,7 +11,9 @@ [dsDebounce]="debounceTime" (onDebounce)="find($event)" [placeholder]="placeholder" [ngModelOptions]="{standalone: true}" autocomplete="off"/> - +

{{'admin.registries.schema.fields.table.selected' | translate}} {{'admin.registries.schema.fields.table.id' | translate}} {{'admin.registries.schema.fields.table.field' | translate}} {{'admin.registries.schema.fields.table.scopenote' | translate}}
- + + {{field.id}} {{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}
- + diff --git a/src/app/shared/object-select/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html index 8cdc8ffb2d3..d1d454b2e43 100644 --- a/src/app/shared/object-select/item-select/item-select.component.html +++ b/src/app/shared/object-select/item-select/item-select.component.html @@ -11,7 +11,7 @@
{{'collection.select.table.selected' | translate}} {{'collection.select.table.title' | translate}}
- + diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index b608c0168a3..47e1c14d46b 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -7,15 +7,29 @@
- -
- - - - - - -
+ +
diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 1bfc1dfa594..a838f51e692 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -167,7 +167,7 @@ export class PaginationComponent implements OnDestroy, OnInit { /** * Number of items per page. */ - public pageSize$; + public pageSize$: Observable; /** * Declare SortDirection enumeration to use it in the template @@ -188,7 +188,7 @@ export class PaginationComponent implements OnDestroy, OnInit { /** * Name of the field that's used to sort by */ - public sortField$; + public sortField$: Observable; public defaultSortField = 'name'; /** diff --git a/src/app/shared/resource-policies/entry/resource-policy-entry.component.html b/src/app/shared/resource-policies/entry/resource-policy-entry.component.html index a14dde366e6..9ad019c8c68 100644 --- a/src/app/shared/resource-policies/entry/resource-policy-entry.component.html +++ b/src/app/shared/resource-policies/entry/resource-policy-entry.component.html @@ -5,8 +5,8 @@ [id]="entry.id" [ngModel]="entry.checked" (ngModelChange)="this.toggleCheckbox.emit($event);"> -
{{'item.select.table.selected' | translate}} {{'item.select.table.collection' | translate}} {{'item.select.table.author' | translate}} {{'item.select.table.title' | translate}}