Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add optional collection home page and descriptive collection content fields #3256

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@ collection:
showSidebar: true
edit:
undoTimeout: 10000 # 10 seconds
# Routing config for optional collection home page
routeThrough:
collectionHomePage: false

# Theme Config
themes:
Expand Down
20 changes: 20 additions & 0 deletions src/app/collection-page/collection-form/collection-form.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,24 @@ export const collectionFormModels: DynamicFormControlModel[] = [
name: 'dc.rights.license',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'customfooter',
name: 'dspace.collection.customfooter',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'headertext',
name: 'dspace.collection.headertext',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'homepageintrotext',
name: 'dspace.collection.homepageintrotext',
spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'ownername',
name: 'dspace.collection.ownername',
spellCheck: environment.form.spellCheck,
}),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<div class="container">
<div class="collection-page"
*ngVar="(collectionRD$ | async) as collectionRD">
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
<div *ngIf="collectionRD?.payload as collection">
<ds-view-tracker [object]="collection"></ds-view-tracker>
<div class="d-flex flex-row border-bottom mb-4 pb-4">
<header class="comcol-header mr-auto">
<!-- Collection Name -->
<ds-comcol-page-header
[name]="dsoNameService.getName(collection)">
</ds-comcol-page-header>
<!-- header text -->
<ds-comcol-page-content
[innerHTML]="sanitizeCustomHeaderText(collection.customHeaderText)">
</ds-comcol-page-content>
<!-- intro text -->
<ds-comcol-page-content
[innerHTML]="sanitizeCustomIntrotext(collection.customHomePageIntroText)">
</ds-comcol-page-content>
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload"
[alternateText]="'collection.logo' | translate">
</ds-comcol-page-logo>
<!-- News -->
<ds-comcol-page-content
[content]="collection.sidebarText"
[hasInnerHtml]="true"
[title]="'collection.page.news'">
</ds-comcol-page-content>
</header>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<section class="comcol-page-browse-section">
<!-- Browse-By Links -->
<ds-comcol-page-browse-by
[id]="collection.id"
[contentType]="collection.type">
</ds-comcol-page-browse-by>

<router-outlet></router-outlet>
</section>
<footer *ngIf="(collection.copyrightText || collection.customOwnerNameText || collection.customFooterText)" class="border-top my-5 pt-4">
<!-- owner name -->
<ds-comcol-page-content
[innerHTML]="sanitizeOwnerNameText(collection.customOwnerNameText)">
</ds-comcol-page-content>
<!-- footer -->
<ds-comcol-page-content
[innerHTML]="sanitizeCustomFootertext(collection.customFooterText)">
</ds-comcol-page-content>
<!-- Copyright -->
<ds-comcol-page-content
[content]="collection.copyrightText"
[hasInnerHtml]="true">
</ds-comcol-page-content>
</footer>
</div>
</div>
<ds-error *ngIf="collectionRD?.hasFailed"
message="{{'error.collection' | translate}}"></ds-error>
<ds-loading *ngIf="collectionRD?.isLoading"
message="{{'loading.collection' | translate}}"></ds-loading>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
OnInit,
SecurityContext,
} from '@angular/core';
import { DomSanitizer} from '@angular/platform-browser';

Check failure on line 11 in src/app/collection-page/collection-home/collection-home-page.component.ts

View workflow job for this annotation

GitHub Actions / tests (18.x)

A space is required before '}'

Check failure on line 11 in src/app/collection-page/collection-home/collection-home-page.component.ts

View workflow job for this annotation

GitHub Actions / tests (20.x)

A space is required before '}'
import {
ActivatedRoute,
Router,
RouterOutlet,
} from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import {
filter,
map,
mergeMap,
take,
} from 'rxjs/operators';

import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { SortOptions } from '../../core/cache/models/sort-options.model';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { RemoteData } from '../../core/data/remote-data';
import { redirectOn4xx } from '../../core/shared/authorized.operators';
import { Bitstream } from '../../core/shared/bitstream.model';
import { Collection } from '../../core/shared/collection.model';
import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators';
import {
fadeIn,
fadeInOut,
} from '../../shared/animations/fade';
import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component';
import { ThemedComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/themed-comcol-page-content.component';
import { ThemedComcolPageHandleComponent } from '../../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component';
import { ComcolPageHeaderComponent } from '../../shared/comcol/comcol-page-header/comcol-page-header.component';
import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component';
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
import {
hasValue,
isNotEmpty,
} from '../../shared/empty.util';
import { ErrorComponent } from '../../shared/error/error.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { ObjectCollectionComponent } from '../../shared/object-collection/object-collection.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { VarDirective } from '../../shared/utils/var.directive';
import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
import { getCollectionPageRoute } from '../collection-page-routing-paths';

@Component({
selector: 'ds-base-collection-home-page',
styleUrls: ['./collection-home-page.component.scss'],
templateUrl: './collection-home-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
fadeIn,
fadeInOut,
],
imports: [
ThemedComcolPageContentComponent,
ErrorComponent,
NgIf,
ThemedLoadingComponent,
TranslateModule,
ViewTrackerComponent,
VarDirective,
AsyncPipe,
ComcolPageHeaderComponent,
ComcolPageLogoComponent,
ThemedComcolPageHandleComponent,
DsoEditMenuComponent,
ThemedComcolPageBrowseByComponent,
ObjectCollectionComponent,
RouterOutlet,
],
standalone: true,
})
export class CollectionHomePageComponent implements OnInit {
collectionRD$: Observable<RemoteData<Collection>>;
logoRD$: Observable<RemoteData<Bitstream>>;
paginationConfig: PaginationComponentOptions;
sortConfig: SortOptions;

/**
* Whether the current user is a Community admin
*/
isCollectionAdmin$: Observable<boolean>;

/**
* Route to the community page
*/
collectionPageRoute$: Observable<string>;

constructor(
protected route: ActivatedRoute,
protected router: Router,
protected authService: AuthService,
protected authorizationDataService: AuthorizationDataService,
public dsoNameService: DSONameService,
protected sanitizer: DomSanitizer,
) {
}

ngOnInit(): void {
this.collectionRD$ = this.route.data.pipe(
map((data) => data.dso as RemoteData<Collection>),
redirectOn4xx(this.router, this.authService),
take(1),
);
this.logoRD$ = this.collectionRD$.pipe(
map((rd: RemoteData<Collection>) => rd.payload),
filter((collection: Collection) => hasValue(collection)),
mergeMap((collection: Collection) => collection.logo),
);
this.isCollectionAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCollectionAdmin);

this.collectionPageRoute$ = this.collectionRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((collection) => getCollectionPageRoute(collection.id)),
);
}

isNotEmpty(object: any) {
return isNotEmpty(object);
}

public sanitizeCustomHeaderText(value) {
return this.sanitizer.sanitize(SecurityContext.HTML, value);
}
public sanitizeCustomIntrotext(value) {
return this.sanitizer.sanitize(SecurityContext.HTML, value);
}
public sanitizeOwnerNameText(value) {
return this.sanitizer.sanitize(SecurityContext.HTML, value);
}
public sanitizeCustomFootertext(value) {
return this.sanitizer.sanitize(SecurityContext.HTML, value);
}
}
6 changes: 6 additions & 0 deletions src/app/collection-page/collection-page-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-s
import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { MenuItemType } from '../shared/menu/menu-item-type.model';
import { CollectionHomePageComponent } from './collection-home/collection-home-page.component';
import { collectionPageResolver } from './collection-page.resolver';
import { collectionPageAdministratorGuard } from './collection-page-administrator.guard';
import {
COLLECTION_CREATE_PATH,
COLLECTION_EDIT_PATH,
COLLECTION_HOME_PATH,
ITEMTEMPLATE_PATH,
} from './collection-page-routing-paths';
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
Expand Down Expand Up @@ -80,6 +82,10 @@ export const ROUTES: Route[] = [
},
data: { title: 'collection.edit.template.title', breadcrumbKey: 'collection.edit.template' },
},
{
path: COLLECTION_HOME_PATH,
component: CollectionHomePageComponent,
},
{
path: '',
component: ThemedCollectionPageComponent,
Expand Down
5 changes: 5 additions & 0 deletions src/app/collection-page/collection-page-routing-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export function getCollectionItemTemplateRoute(id) {
return new URLCombiner(getCollectionPageRoute(id), ITEMTEMPLATE_PATH).toString();
}

export function getCollectionHomeRoute(id) {
return new URLCombiner(getCollectionPageRoute(id), COLLECTION_HOME_PATH).toString();
}

export const COLLECTION_CREATE_PATH = 'create';
export const COLLECTION_EDIT_PATH = 'edit';
export const COLLECTION_EDIT_ROLES_PATH = 'roles';
export const ITEMTEMPLATE_PATH = 'itemtemplate';
export const COLLECTION_HOME_PATH = 'home';
32 changes: 32 additions & 0 deletions src/app/core/shared/collection.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,38 @@ export class Collection extends DSpaceObject implements ChildHALResource, Handle
return this.firstMetadataValue('dc.rights.license');
}

/**
* The header text of this collection
* Corresponds to the metadata field dspace.collection.headertext
*/
get customHeaderText(): string {
return this.firstMetadataValue('dspace.collection.headertext');
}

/**
* The custom footer of this collection
* Corresponds to the metadata field dspace.collection.customfooter
*/
get customFooterText(): string {
return this.firstMetadataValue('dspace.collection.customfooter');
}

/**
* The home page intro text of this collection
* Corresponds to the metadata field dspace.collection.homepageintrotext
*/
get customHomePageIntroText(): string {
return this.firstMetadataValue('dspace.collection.homepageintrotext');
}

/**
* The owner name of this collection
* Corresponds to the metadata field dspace.collection.introtext
*/
get customOwnerNameText(): string {
return this.firstMetadataValue('dspace.collection.ownername');
}

/**
* The sidebar text of this Collection
* Corresponds to the metadata field dc.description.tableofcontents
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<div class="d-flex flex-row">
<a *ngIf="linkType !== linkTypes.None" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/collections/' + object.id]" class="lead">
<a *ngIf="(linkType !== linkTypes.None) && (useCollectionHomePage !== true)" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/collections/' + object.id]" class="lead">
{{ dsoNameService.getName(object) }}
</a>
<a *ngIf="(linkType !== linkTypes.None) && (useCollectionHomePage === true)" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="['/collections/' + object.id + '/home']" class="lead">
{{ dsoNameService.getName(object) }}
</a>
<span *ngIf="linkType === linkTypes.None" class="lead">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { APP_CONFIG } from 'src/config/app-config.interface';
import { environment } from 'src/environments/environment.test';

import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { Collection } from '../../../core/shared/collection.model';
Expand Down Expand Up @@ -71,6 +73,7 @@ describe('CollectionListElementComponent', () => {
providers: [
{ provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract) },
{ provide: APP_CONFIG, useValue: environment },
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
],
schemas: [NO_ERRORS_SCHEMA],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { NgIf } from '@angular/common';
import { Component } from '@angular/core';
import {
Component,
Inject,
OnInit,
} from '@angular/core';
import { RouterLink } from '@angular/router';
import {
APP_CONFIG,
AppConfig,
} from 'src/config/app-config.interface';

import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { Collection } from '../../../core/shared/collection.model';
import { ViewMode } from '../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
Expand All @@ -18,4 +27,18 @@ import { AbstractListableElementComponent } from '../../object-collection/shared
* Component representing list element for a collection
*/
@listableObjectComponent(Collection, ViewMode.ListElement)
export class CollectionListElementComponent extends AbstractListableElementComponent<Collection> {}
export class CollectionListElementComponent extends AbstractListableElementComponent<Collection> implements OnInit {

useCollectionHomePage: boolean;

constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
public dsoNameService: DSONameService,
) {
super(dsoNameService);
}

ngOnInit(): void {
this.useCollectionHomePage = this.appConfig.collection.routeThrough.collectionHomePage;
}
}
Loading
Loading