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
12 changes: 11 additions & 1 deletion config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@ homePage:
editHomeNews: true
# Disable if you don't want users to edit the home header.
editHomeHeader: true
# Enable or disable the dynamic layout configuration on the homepage
enableDynamicLayout: false

# Item Config
item:
Expand Down Expand Up @@ -714,8 +716,11 @@ accessibility:
cookieExpirationDuration: 7

# Configuration for layout customization of metadata rendering in Item page
# Currently only the authority reference config is available, more will follow with the integration of the so called CRIS layout.
# Currently only the authority reference config is available, more will follow with the integration of the so called dynamic layout.
layout:
# Enable or disable the explore pages feature (/explore/:id routes and their navbar menu entries).
# When false, explore routes redirect to 404 and the explore menu is hidden.
enableExplorePages: false
# Configuration of icons and styles to be used for each authority controlled link
authorityRef:
- entityType: DEFAULT
Expand Down Expand Up @@ -763,6 +768,11 @@ layout:
type: attribute
- name: checksum
type: attribute
# Navbar layout configuration
navbar:
# If true, show the "Community and Collections" link in the navbar; otherwise, show it in the admin sidebar
showCommunityCollection: true


# Configuration for customization of search results
searchResults:
Expand Down
4 changes: 4 additions & 0 deletions src/app/app-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ export const APP_ROUTES: Route[] = [
.then((m) => m.ROUTES),
canActivate: [notAuthenticatedGuard],
},
{
path: 'explore',
loadChildren: () => import('./explore-page/explore-routes').then((m) => m.ROUTES),
},
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent, data: { title: '404.page-not-found' } },
],
},
Expand Down
4 changes: 4 additions & 0 deletions src/app/app.menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { buildMenuStructure } from './shared/menu/menu.structure';
import { MenuID } from './shared/menu/menu-id.model';
import { MenuRoute } from './shared/menu/menu-route.model';
import { AccessControlMenuProvider } from './shared/menu/providers/access-control.menu';
import { AdminCommunityListMenuProvider } from './shared/menu/providers/admin-community-list.menu';
import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.menu';
import { AuditLogsMenuProvider } from './shared/menu/providers/audit-item.menu';
import { AuditOverviewMenuProvider } from './shared/menu/providers/audit-overview.menu';
Expand All @@ -25,6 +26,7 @@ import { EditMenuProvider } from './shared/menu/providers/edit.menu';
import { EditCMSMetadataMenuProvider } from './shared/menu/providers/edit-cms-metadata.menu';
import { EditItemMenuProvider } from './shared/menu/providers/edit-item-details.menu';
import { EditUserAgreementMenuProvider } from './shared/menu/providers/edit-user-agreement.menu';
import { ExploreMenuProvider } from './shared/menu/providers/explore.menu';
import { ExportMenuProvider } from './shared/menu/providers/export.menu';
import { HealthMenuProvider } from './shared/menu/providers/health.menu';
import { ImportMenuProvider } from './shared/menu/providers/import.menu';
Expand Down Expand Up @@ -61,8 +63,10 @@ export const MENUS = buildMenuStructure({
CommunityListMenuProvider,
BrowseMenuProvider,
StatisticsMenuProvider,
ExploreMenuProvider,
],
[MenuID.ADMIN]: [
AdminCommunityListMenuProvider,
NewMenuProvider,
EditMenuProvider,
ImportMenuProvider,
Expand Down
Empty file.
47 changes: 47 additions & 0 deletions src/app/core/data/section-data.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
import { NotificationsService } from '@dspace/core/notification-system/notifications.service';
import { Observable } from 'rxjs';

import { DSONameService } from '../breadcrumbs/dso-name.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { Section } from '../layout/models/section.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { IdentifiableDataService } from './base/identifiable-data.service';
import { SearchDataImpl } from './base/search-data';
import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
import { RequestService } from './request.service';

/**
* A service responsible for fetching data from the REST API on the sections endpoint.
*/
@Injectable({ providedIn: 'root' })
export class SectionDataService extends IdentifiableDataService<Section> {

protected linkPath = 'sections';
private searchData: SearchDataImpl<Section>;

constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected dsoNameService: DSONameService,
) {
super('sections', requestService, rdbService, objectCache, halService);

this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}


/**
* Finds all sections configured to be visible in the top navigation bar.
* Uses the 'visibleTopBarSections' search endpoint on the backend.
*/
findVisibleSections(): Observable<RemoteData<PaginatedList<Section>>> {
return this.searchData.searchBy('visibleTopBarSections');
}

}
161 changes: 161 additions & 0 deletions src/app/core/layout/models/section.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import {
autoserialize,
deserialize,
} from 'cerialize';

import { typedObject } from '../../cache/builders/build-decorators';
import { CacheableObject } from '../../cache/cacheable-object.model';
import { HALLink } from '../../shared/hal-link.model';
import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { SECTION } from './section.resource-type';

/**
* Describes a type of Section.
*/
@typedObject
export class Section extends CacheableObject {
static type = SECTION;

/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;

/**
* The identifier of this Section.
*/
@autoserialize
id: string;

@autoserialize
componentRows: SectionComponent[][];

@autoserialize
nestedSections: Section[];

/**
* The {@link HALLink}s for this section
*/
@deserialize
_links: {
self: HALLink,
};

}

/**
* Base interface for all section component configurations returned by the REST API.
*/
export interface SectionComponent {
/** The type discriminator identifying which component to render. */
componentType: string;
/** CSS classes applied to the section container (e.g., Bootstrap grid classes). */
style: string;
}

/**
* Configuration for a browse section that displays links to browse indices.
*/
export interface BrowseSection extends SectionComponent {
/** List of browse index names to render as navigation links. */
browseNames: string[];
componentType: 'browse';
}

/**
* Configuration for a top section that displays the most recent/popular items
* from a discovery configuration.
*/
export interface TopSection extends SectionComponent {
/** Name of the discovery configuration to query. */
discoveryConfigurationName: string;
/** Metadata field to sort results by. */
sortField: string;
/** Sort direction ('ASC' or 'DESC'). */
order: string;
/** i18n key for the section title. */
titleKey: string;
componentType: 'top';
/** Maximum number of items to display. */
numberOfItems: number;
/** Whether to show item thumbnails. */
showThumbnails: boolean;
/** Template type to use for rendering the items. */
template: TopSectionTemplateType;
}

/**
* Configuration for an advanced search section with multiple query statements and filter fields.
*/
export interface SearchSection extends SectionComponent {
/** Name of the discovery configuration providing available filters. */
discoveryConfigurationName: string;
componentType: 'search';
/** Type of search interface to render. */
searchType: string;
/** Number of query statement rows to display initially. */
initialStatements: number;
/** Whether to display the section title. */
displayTitle: boolean;
}

/**
* Configuration for a section displaying search facets from a discovery configuration.
*/
export interface FacetSection extends SectionComponent {
/** Name of the discovery configuration to fetch facet values from. */
discoveryConfigurationName: string;
componentType: 'facet';
/** Number of facet boxes to display per row in the grid. */
facetsPerRow: number;
}

/**
* Configuration for a text content section that renders static or metadata-based content.
*/
export interface TextRowSection extends SectionComponent {
/** The content string (can be a metadata key or raw content depending on contentType). */
content: string;
/** The type of content: e.g., 'text-metadata' for metadata lookups or 'text-raw' for static content. */
contentType: string;
componentType: 'text-row';
}

/**
* Configuration for a table-like section showing top items with multiple metadata columns.
*/
export interface MultiColumnTopSection extends SectionComponent {
/** Name of the discovery configuration to query. */
discoveryConfigurationName: string;
/** Metadata field to sort results by. */
sortField: string;
/** Sort direction ('ASC' or 'DESC'). */
order: string;
/** i18n key for the section title. */
titleKey: string;
/** List of column definitions specifying which metadata fields to display. */
columnList: TopSectionColumn[];
componentType: 'multi-column-top';
}

/**
* Column configuration for {@link MultiColumnTopSection} defining which metadata to display.
*/
export interface TopSectionColumn {
/** CSS classes for column width styling. */
style: string;
/** Metadata field to extract the column value from. */
metadataField: string;
/** i18n key for the column header. */
titleKey: string;
}

/**
* Represents the type of template to use for the section
*/
export enum TopSectionTemplateType {
DEFAULT = 'default',
}
9 changes: 9 additions & 0 deletions src/app/core/layout/models/section.resource-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ResourceType } from '../../shared/resource-type';

/**
* The resource type for Section
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const SECTION = new ResourceType('section');
2 changes: 2 additions & 0 deletions src/app/core/provide-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { EPerson } from './eperson/models/eperson.model';
import { Group } from './eperson/models/group.model';
import { Feedback } from './feedback/models/feedback.model';
import { Section } from './layout/models/section.model';
import { MetadataField } from './metadata/metadata-field.model';
import { MetadataSchema } from './metadata/metadata-schema.model';
import { QualityAssuranceEventObject } from './notifications/qa/models/quality-assurance-event.model';
Expand Down Expand Up @@ -232,4 +233,5 @@ export const models =
CorrectionType,
SupervisionOrder,
SubmissionCustomUrl,
Section,
];
74 changes: 74 additions & 0 deletions src/app/core/services/internal-link.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
TestBed,
waitForAsync,
} from '@angular/core/testing';

import { InternalLinkService } from './internal-link.service';
import { NativeWindowService } from './window.service';

describe('InternalLinkService', () => {
let service: InternalLinkService;

beforeEach(waitForAsync(() => {
return TestBed.configureTestingModule({
providers: [
InternalLinkService,
{ provide: NativeWindowService, useValue: { nativeWindow: { location: { origin: 'https://currentdomain' } } } },
],
}).compileComponents();
}));

beforeEach(() => {
service = TestBed.inject(InternalLinkService);
});

describe('isLinkInternal', () => {
it('should return true for internal link starting with "/"', () => {
const result = service.isLinkInternal('/my-link');
expect(result).toBe(true);
});

it('should return true for internal link starting with currentURL', () => {
const result = service.isLinkInternal('https://currentdomain/my-link');
expect(result).toBe(true);
});

it('should return true for internal link starting with "currentdomain"', () => {
const result = service.isLinkInternal('currentdomain/my-link');
expect(result).toBe(true);
});

it('should return false for external link', () => {
const result = service.isLinkInternal('https://externaldomain/my-link');
expect(result).toBe(false);
});

it('should return true for internal link without leading "/"', () => {
const result = service.isLinkInternal('my-link');
expect(result).toBe(true);
});
});

describe('transformInternalLink', () => {
it('should transform internal link by removing currentURL', () => {
const result = service.getRelativePath('https://currentdomain/my-link');
expect(result).toBe('/my-link');
});

it('should transform internal link by adding leading "/" if missing', () => {
const result = service.getRelativePath('currentdomain/my-link');
expect(result).toBe('/my-link');
});

it('should return unchanged link for external link', () => {
const result = service.getRelativePath('https://externalDomain/my-link');
expect(result).toBe('/https://externalDomain/my-link');
});

it('should return unchanged link for internal link with leading "/"', () => {
const result = service.getRelativePath('/my-link');
expect(result).toBe('/my-link');
});
});

});
Loading
Loading