Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -805,3 +805,182 @@ addToAnyPlugin:
showPlusButton: true
showCounters: true
title: DSpace demo

# Config-driven item-page field layout (see resolveItemPageLayout / ItemPageFieldListComponent
# under src/themes/qulto/app/item-page/simple/field-components/dynamic/). Keyed by entity type
# (e.g. `Publication`, or `Item` for untyped items) — root-level rather than per-theme, since a
# deployment only ever runs one active theme. Currently only the untyped-item ("Item") page
# consumes this; Publication pages are not migrated yet and still render their field list from HTML.
itemPage:
Item:
leftSide:
#- container: date
- container: metadata-representation-list
itemType: Person
fields: [ dc.contributor.author, dc.creator ]
label: item.page.authors
- container: generic
fields: [ dc.description.version ]
label: item.page.version
- container: generic
fields: [ journal.title ]
label: item.page.journal-title
- container: generic
fields: [ journal.identifier.issn ]
label: item.page.journal-issn
- container: generic
fields: [ journalvolume.identifier.name ]
label: item.page.volume-title
- container: generic
fields: [ dc.publisher ]
label: item.page.publisher
- container: doi
rightSide:
- container: abstract
- container: generic
fields: [ dc.publisher ]
label: item.page.publisher
- container: generic
fields: [ dc.description ]
label: item.page.description
- container: generic
fields: [ dc.subject ]
separator: ', '
label: item.page.subject
- container: generic
fields: [ dc.identifier.citation ]
label: item.page.citation
- container: geospatial
label: item.page.places
pointFields: [ dcterms.spatial ]
bboxFields: [ dcterms.spatial ]
cluster: true
- container: uri
fields: [ dc.identifier.uri ]
label: item.page.uri
- container: generic
fields: [ dc.description.version ]
label: item.page.version
- container: collections
- container: uri
fields: [ coar.notify.endorsedBy ]
label: item.page.endorsement
- container: uri
fields: [ datacite.relation.isReviewedBy ]
label: item.page.review
- container: uri
fields: [ datacite.relation.isSupplementedBy ]
label: item.page.supplemented
- container: uri
fields: [ datacite.relation.isReferencedBy ]
label: item.page.referenced
- container: cc-license
variant: full

Publication:
leftSide:
#- container: date
- container: metadata-representation-list
itemType: Person
fields: [ dc.contributor.author, dc.creator ]
label: relationships.isAuthorOf
- container: generic
fields: [ journal.title ]
label: publication.page.journal-title
- container: generic
fields: [ journal.identifier.issn ]
label: publication.page.journal-issn
- container: generic
fields: [ journalvolume.identifier.name ]
label: publication.page.volume-title
- container: generic
fields: [ dc.publisher ]
label: publication.page.publisher
- container: doi
rightSide:
- container: related-items
relationType: isProjectOfPublication
label: item.page.projects
- container: related-items
relationType: isOrgUnitOfPublication
label: item.page.org-units
- container: related-items
relationType: isJournalIssueOfPublication
label: item.page.journal-issue
- container: related-items
relationType: isPartOf
label: relationships.isPartOf
- container: abstract
- container: generic
fields: [ dc.publisher ]
label: item.page.publisher
- container: generic
fields: [ dc.description ]
label: publication.page.description
- container: generic
fields: [ dc.subject ]
separator: ', '
label: item.page.subject
- container: generic
fields: [ dc.identifier.citation ]
label: item.page.citation
- container: uri
fields: [ dc.identifier.uri ]
label: item.page.uri
- container: collections
- container: uri
fields: [ coar.notify.endorsedBy ]
label: item.page.endorsement
- container: uri
fields: [ datacite.relation.isReviewedBy ]
label: item.page.review
- container: uri
fields: [ datacite.relation.isSupplementedBy ]
label: item.page.supplemented
- container: uri
fields: [ datacite.relation.isReferencedBy ]
label: item.page.referenced
- container: license
- container: geospatial
label: item.page.places
pointFields: [ dcterms.spatial ]
bboxFields: [ dcterms.spatial ]
cluster: true
# fullWidth is disabled by default (this mirrors what used to be a commented-out block in
# publication.component.html) — uncomment to show the isPartOf tabbed related-entities
# search below the two columns:
# fullWidth:
# - container: tabbed-related-entities-search
# relationTypes:
# - { label: isPartOf, filter: isPartOf, configuration: default-relationships }

# Config-driven fields for the search-result list element's subtitle line (see
# resolveItemSearchResultListElementConfig under src/themes/qulto/app/shared/object-list/
# search-result-list-element/item-search-result/item-types/item/). Applies to both Publication
# and generic Item search results — one shared layout, not per entity type. `authors.cssClass`/
# `authors.separator` only control styling; WHICH fields count as "author" is still driven by
# the existing `searchResult.authorMetadata` app config (default-app-config.ts;
# not overridden in this file today).
itemSearchResultListElement:
group:
prefix: '('
suffix: ')'
separator: ', '
fields:
- fields: [ dc.publisher ]
cssClass: item-list-publisher
- fields: [ dc.date.issued ]
cssClass: item-list-date
# Freestanding fields — same {fields, cssClass} shape as `group`, but NOT wrapped in
# parens/joined with publisher+date. Use this to add a new field without touching `group`.
inlineFields:
fields:
- fields: [ dc.description.version ]
cssClass: item-list-version
authors:
cssClass: item-list-authors
separator: '; '
abstract:
fields: [ dc.description.abstract ]
cssClass: item-list-abstract
minLines: 3
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { resolveItemPageLayout } from './item-page-config.util';

describe('resolveItemPageLayout', () => {
it('returns an empty layout when itemPage is not configured', () => {
expect(resolveItemPageLayout({}, 'Publication')).toEqual({});
});

it('returns an empty layout when the entity type is not configured', () => {
const config = { itemPage: { Item: { leftSide: [{ container: 'date' }] } } };
expect(resolveItemPageLayout(config, 'Publication')).toEqual({});
});

it('returns the configured layout for the entity type', () => {
const layout = { leftSide: [{ container: 'date' }], rightSide: [{ container: 'abstract' }] };
expect(resolveItemPageLayout({ itemPage: { Item: layout } }, 'Item')).toEqual(layout);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AppConfig } from '@dspace/config/app-config.interface';

import { ItemPageLayoutConfig } from './item-page-field.config';

/**
* Look up the item-page field layout for one entity type from the root-level `itemPage`
* config key. A deployment only ever runs one active theme, so there is no per-theme
* nesting or `extends` chain to resolve here — just a direct config lookup.
*/
export function resolveItemPageLayout(config: Pick<AppConfig, 'itemPage'>, entityType: string): ItemPageLayoutConfig {
return config.itemPage?.[entityType] ?? {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Type } from '@angular/core';

import { CollectionsComponent } from '../../../../../../../app/item-page/field-components/collections/collections.component';
import { ItemPageAbstractFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component';
import { ItemPageCcLicenseFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component';
import { ItemPageDateFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/date/item-page-date-field.component';
import { GenericItemPageFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component';
import { GeospatialItemPageFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/geospatial/geospatial-item-page-field.component';
import { ItemPageLicenseFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/license/item-page-license-field.component';
import { ItemPageUriFieldComponent } from '../../../../../../../app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component';
import { ThemedMetadataRepresentationListComponent } from '../../../../../../../app/item-page/simple/metadata-representation-list/themed-metadata-representation-list.component';
import { TabbedRelatedEntitiesSearchComponent } from '../../../../../../../app/item-page/simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component';
import { RelatedItemsComponent } from '../../../../../../../app/item-page/simple/related-items/related-items-component';
import { ItemPageDoiFieldComponent } from '../specific-field/doi/item-page-doi-field.component';

/**
* Maps an `ItemPageFieldConfig.container` key (from `config.yml`) to the component it renders
* and the `@Input()` name the item/parent-item is bound to — most containers take `item`, but
* the two relation-list containers take `parentItem`.
*/
export interface ItemPageFieldContainerDef {
component: Type<any>;
itemInputName: 'item' | 'parentItem';
}

export const ITEM_PAGE_FIELD_CONTAINER_REGISTRY: Record<string, ItemPageFieldContainerDef> = {
'generic': { component: GenericItemPageFieldComponent, itemInputName: 'item' },
'uri': { component: ItemPageUriFieldComponent, itemInputName: 'item' },
'abstract': { component: ItemPageAbstractFieldComponent, itemInputName: 'item' },
'date': { component: ItemPageDateFieldComponent, itemInputName: 'item' },
'license': { component: ItemPageLicenseFieldComponent, itemInputName: 'item' },
'cc-license': { component: ItemPageCcLicenseFieldComponent, itemInputName: 'item' },
'doi': { component: ItemPageDoiFieldComponent, itemInputName: 'item' },
'geospatial': { component: GeospatialItemPageFieldComponent, itemInputName: 'item' },
'collections': { component: CollectionsComponent, itemInputName: 'item' },
'metadata-representation-list': { component: ThemedMetadataRepresentationListComponent, itemInputName: 'parentItem' },
'related-items': { component: RelatedItemsComponent, itemInputName: 'parentItem' },
'tabbed-related-entities-search': { component: TabbedRelatedEntitiesSearchComponent, itemInputName: 'item' },
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@for (cfg of visibleConfigs; track $index) {
<ng-container *ngComponentOutlet="componentFor(cfg); inputs: inputsFor(cfg)"></ng-container>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { NgComponentOutlet } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Input,
Type,
} from '@angular/core';
import { Item } from '@dspace/core/shared/item.model';

import { ItemPageFieldConfig } from './item-page-field.config';
import { ITEM_PAGE_FIELD_CONTAINER_REGISTRY } from './item-page-field-container.registry';

/**
* Renders an ordered list of item-page metadata/relation fields declared in `config.yml`
* (`ItemPageFieldConfig[]`, see `resolveItemPageLayout`). Each entry's `container` key is
* resolved to a component via `ITEM_PAGE_FIELD_CONTAINER_REGISTRY`; every other property on
* the entry is passed through as that component's input.
*/
@Component({
selector: 'ds-item-page-field-list',
templateUrl: './item-page-field-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
NgComponentOutlet,
],
})
export class ItemPageFieldListComponent {
@Input() item: Item;

@Input() configs: ItemPageFieldConfig[] = [];

/**
* Gate for the `geospatial` container. The base item-type components already compute
* `geospatialItemPageFieldsEnabled` from `environment.geospatialMapViewer`; callers pass
* that through here instead of this component reading `environment` itself, so it stays
* a plain config-driven renderer.
*/
@Input() geospatialEnabled = true;

get visibleConfigs(): ItemPageFieldConfig[] {
return this.configs.filter((cfg) =>
!!ITEM_PAGE_FIELD_CONTAINER_REGISTRY[cfg.container] &&
(cfg.container !== 'geospatial' || this.geospatialEnabled),
);
}

componentFor(cfg: ItemPageFieldConfig): Type<any> {
return ITEM_PAGE_FIELD_CONTAINER_REGISTRY[cfg.container].component;
}

/**
* Only forwards explicitly-set config properties: passing `undefined` through
* `ngComponentOutletInputs` would overwrite the target component's own default value
* (e.g. `separator`) instead of leaving it alone.
*/
inputsFor(cfg: ItemPageFieldConfig): Record<string, unknown> {
const { container, ...rest } = cfg;
const inputs: Record<string, unknown> = {
[ITEM_PAGE_FIELD_CONTAINER_REGISTRY[container].itemInputName]: this.item,
};
for (const [key, value] of Object.entries(rest)) {
if (value !== undefined) {
inputs[key] = value;
}
}
return inputs;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Config } from '@dspace/config/config.interface';

/**
* A single field/relation rendered on an item page, resolved to one of the container
* components registered in `ITEM_PAGE_FIELD_CONTAINER_REGISTRY`. Properties beyond
* `container` are passed straight through as that component's `@Input()`s, so which
* ones apply depends on the chosen container (e.g. `fields` for `generic`/`uri`,
* `relationType` for `related-items`, `pointFields`/`bboxFields`/`cluster` for `geospatial`).
*/
export interface ItemPageFieldConfig extends Config {
container: string;
label?: string;
fields?: string[];
separator?: string;
urlRegex?: string;
enableMarkdown?: boolean;
itemType?: string;
metadataFields?: string[];
incrementBy?: number;
relationType?: string;
relationTypes?: { label: string; filter: string; configuration?: string }[];
variant?: 'small' | 'full';
ccLicenseUriField?: string;
ccLicenseNameField?: string;
showName?: boolean;
showDisclaimer?: boolean;
pointFields?: string[];
bboxFields?: string[];
cluster?: boolean;
searchEnabled?: boolean;
sideBarWidth?: number;
}

/**
* Ordered field lists for one entity type's item page. `leftSide`/`rightSide` map to the
* two metadata columns; `fullWidth` renders below both (e.g. kjk's tabbed related-entities
* search).
*/
export interface ItemPageLayoutConfig extends Config {
leftSide?: ItemPageFieldConfig[];
rightSide?: ItemPageFieldConfig[];
fullWidth?: ItemPageFieldConfig[];
}

/**
* Root `config.yml` `itemPage` key, keyed by entity type (e.g. `Publication`, or `Item` for
* untyped items). Declared once at the top level rather than per-theme: a single deployment
* only ever runs one active theme, so there is no need to namespace this by theme name.
*/
export interface ItemPageConfig extends Config {
[entityType: string]: ItemPageLayoutConfig;
}

/**
* Augments the core `AppConfig` (rather than editing `src/config/app-config.interface.ts`
* directly) so this config-driven item-page feature — and its types — stay self-contained
* under the qulto theme. TypeScript declaration merging picks this up because
* `tsconfig.app.json`/`tsconfig.spec.json` unconditionally include every `.ts` file under `src/themes`.
*/
declare module '@dspace/config/app-config.interface' {
interface AppConfig {
itemPage?: ItemPageConfig;
}
}
Loading
Loading