diff --git a/config/config.yml b/config/config.yml
index 109db60ca92..e0080e872f6 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -3,3 +3,38 @@ rest:
host: sandbox.dspace.org
port: 443
nameSpace: /server
+
+graph-viewer:
+ url: 'https://eltetanrep-graf.dspace.testing.qulto.eu/'
+
+mediaViewer:
+ image: true
+ video: true
+
+themes:
+ - name: elte
+ headTags:
+ - tagName: link
+ attributes:
+ rel: icon
+ href: /assets/qulto/images/qultoIcon.svg
+ sizes: image/svg+xml
+
+search:
+ # Settings to enable/disable or configure Advanced Search filters.
+ advancedFilters:
+ enabled: true
+ # List of filters to enable in "Advanced Search" dropdown
+ filter: [ 'title', 'author', 'subject', 'entityType' ]
+
+
+homePage:
+ topLevelCommunityList:
+ # No. of communities to list per page on the home page
+ # This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
+ pageSize: 4
+ # layout can be "cards" or "list"
+ layout: "list"
+
+item:
+ showAccessStatuses: true
\ No newline at end of file
diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts
index c62aa3253d3..9e5ac0bdbab 100644
--- a/src/app/app-routes.ts
+++ b/src/app/app-routes.ts
@@ -12,6 +12,7 @@ import {
ERROR_PAGE,
FORBIDDEN_PATH,
FORGOT_PASSWORD_PATH,
+ GRAPH_VIEWER_PATH,
HEALTH_PAGE_PATH,
INFO_MODULE_PATH,
INTERNAL_SERVER_ERROR,
@@ -266,6 +267,12 @@ export const APP_ROUTES: Route[] = [
.then((m) => m.ROUTES),
canActivate: [authenticatedGuard],
},
+ {
+ path: GRAPH_VIEWER_PATH,
+ loadComponent: () => import('../themes/elte/app/graph-viewer/graph-viewer.component')
+ .then(m => m.GraphViewerComponent),
+ data: { title: 'graph-viewer.title' },
+ },
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
],
},
diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts
index 7d202f16e9c..1efe001610a 100644
--- a/src/app/app-routing-paths.ts
+++ b/src/app/app-routing-paths.ts
@@ -142,3 +142,4 @@ export function getEditItemPageRoute() {
}
export const CORRECTION_TYPE_PATH = 'corrections';
+export const GRAPH_VIEWER_PATH = 'graph-viewer';
diff --git a/src/themes/elte/app/graph-viewer/graph-viewer.component.scss b/src/themes/elte/app/graph-viewer/graph-viewer.component.scss
new file mode 100644
index 00000000000..a03caf5c5e4
--- /dev/null
+++ b/src/themes/elte/app/graph-viewer/graph-viewer.component.scss
@@ -0,0 +1,28 @@
+:host {
+ display: block;
+ // Prevent the component from generating its own scrollbar
+ overflow: hidden;
+}
+
+.graph-viewer-container {
+ /* Calculate available height: 100vh minus (Header + Footer + potential margins) */
+ /* 160px is a safe estimate for the header and footer; adjust if necessary */
+ height: calc(100vh - 160px);
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.graph-iframe {
+ flex-grow: 1;
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+/* Overwrite DSpace default container padding/max-width to ensure full-width layout */
+:host-context(ds-graph-viewer) .container {
+ max-width: 100% !important;
+ padding: 0 !important;
+ margin: 0 !important;
+}
\ No newline at end of file
diff --git a/src/themes/elte/app/graph-viewer/graph-viewer.component.ts b/src/themes/elte/app/graph-viewer/graph-viewer.component.ts
new file mode 100644
index 00000000000..93ebec1c9c9
--- /dev/null
+++ b/src/themes/elte/app/graph-viewer/graph-viewer.component.ts
@@ -0,0 +1,143 @@
+import {
+ AsyncPipe,
+ NgIf,
+} from '@angular/common';
+import {
+ Component,
+ HostListener,
+ Inject,
+ NgZone,
+ OnDestroy,
+ OnInit,
+} from '@angular/core';
+import {
+ DomSanitizer,
+ SafeResourceUrl,
+} from '@angular/platform-browser';
+import {
+ ActivatedRoute,
+ Router,
+} from '@angular/router';
+import { TranslateModule } from '@ngx-translate/core';
+import { Subscription } from 'rxjs';
+import {
+ APP_CONFIG,
+ AppConfig,
+} from 'src/config/app-config.interface';
+
+@Component({
+ selector: 'ds-graph-viewer',
+ standalone: true,
+ imports: [NgIf, AsyncPipe, TranslateModule],
+ template: `
+
+
+
+ `,
+ styleUrls: ['./graph-viewer.component.scss'],
+})
+export class GraphViewerComponent implements OnInit, OnDestroy {
+ safeUrl: SafeResourceUrl;
+ baseUrl: string;
+ private lastQuery: string | undefined = undefined; // Undefined value for the initial state
+ private subs: Subscription[] = [];
+
+ constructor(
+ @Inject(APP_CONFIG) protected appConfig: AppConfig,
+ private sanitizer: DomSanitizer,
+ private route: ActivatedRoute,
+ private router: Router,
+ private zone: NgZone,
+ ) {}
+
+ ngOnInit(): void {
+ this.baseUrl = (this.appConfig as any)['graph-viewer']?.url;
+
+ if (this.baseUrl) {
+ this.subs.push(
+ this.route.queryParams.subscribe((params) => {
+ const currentQ = params.q || ''; // Normalize to empty string if missing
+
+ // Trigger build only if it's the first load OR an external change (e.g. Back button)
+ if (this.lastQuery === undefined || currentQ !== this.lastQuery) {
+ // console.log('Iframe source update required');
+ this.lastQuery = currentQ;
+ this.buildSafeUrl(currentQ);
+ }
+ }),
+ );
+ }
+ }
+
+ /**
+ * Builds the sanitized URL for the iframe.
+ * @param q The query value to append
+ */
+ private buildSafeUrl(q?: string): void {
+ if (!this.baseUrl) {return;}
+
+ try {
+ const url = new URL(this.baseUrl);
+ const queryParam = q || this.route.snapshot.queryParams.q;
+
+ if (queryParam) {
+ url.searchParams.append('q', queryParam);
+ }
+
+ this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url.toString());
+ } catch (e) {
+ // console.error('Invalid URL format');
+ }
+ }
+
+ @HostListener('window:message', ['$event'])
+ onMessage(event: MessageEvent) {
+ if (!event.data || typeof event.data !== 'object' || !event.data.type) {return;}
+ if (this.baseUrl && !this.baseUrl.startsWith(event.origin)) {return;}
+
+ const { type, data } = event.data;
+
+ if (type === 'SEARCH_CHANGE' && data) {
+ let valueToNavigate = '';
+
+ // Handle different data structures from React
+ if (typeof data === 'object' && data.q && Array.isArray(data.q)) {
+ valueToNavigate = data.q[0];
+ } else if (typeof data === 'string') {
+ valueToNavigate = data;
+ }
+
+ if (valueToNavigate) {
+ // Block the next subscription-based reload
+ this.lastQuery = valueToNavigate;
+
+ this.zone.run(() => {
+ this.updateHostUrl(valueToNavigate);
+ });
+ }
+ }
+
+ if (type === 'OPEN_URL' && data) {
+ this.zone.run(() => {
+ window.open(data, '_blank');
+ });
+ }
+ }
+
+ private updateHostUrl(qValue: string): void {
+ this.router.navigate([], {
+ relativeTo: this.route,
+ queryParams: { q: qValue },
+ queryParamsHandling: 'merge',
+ replaceUrl: true,
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subs.forEach((s) => s.unsubscribe());
+ }
+}
diff --git a/src/themes/elte/app/navbar/navbar.component.ts b/src/themes/elte/app/navbar/navbar.component.ts
index 12b0018b792..1dda55d6c2b 100644
--- a/src/themes/elte/app/navbar/navbar.component.ts
+++ b/src/themes/elte/app/navbar/navbar.component.ts
@@ -5,10 +5,21 @@ import {
NgFor,
NgIf,
} from '@angular/common';
-import { Component } from '@angular/core';
+import {
+ Component,
+ inject,
+ OnDestroy,
+ OnInit,
+} from '@angular/core';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
+import { Subscription } from 'rxjs'; // Fontos import!
+import { GRAPH_VIEWER_PATH } from 'src/app/app-routing-paths';
import { ThemedUserMenuComponent } from 'src/app/shared/auth-nav-menu/user-menu/themed-user-menu.component';
+import { MenuService } from 'src/app/shared/menu/menu.service';
+import { MenuID } from 'src/app/shared/menu/menu-id.model';
+import { LinkMenuItemModel } from 'src/app/shared/menu/menu-item/models/link.model';
+import { MenuItemType } from 'src/app/shared/menu/menu-item-type.model';
import { NavbarComponent as BaseComponent } from '../../../../app/navbar/navbar.component';
import { slideMobileNav } from '../../../../app/shared/animations/slide';
@@ -26,5 +37,50 @@ import { slideMobileNav } from '../../../../app/shared/animations/slide';
standalone: true,
imports: [NgbDropdownModule, NgClass, NgIf, ThemedUserMenuComponent, NgFor, NgComponentOutlet, AsyncPipe, TranslateModule],
})
-export class NavbarComponent extends BaseComponent {
+export class NavbarComponent extends BaseComponent implements OnInit, OnDestroy {
+ // ITT DEKLARÁLJUK a változót, így elűnik a hibaüzenet
+ private menuSubs: Subscription[] = [];
+
+ // A BaseComponent-től örökölt szervizek mellé injektáljuk, amit kell
+ protected menuService = inject(MenuService);
+
+ ngOnInit() {
+ super.ngOnInit();
+
+ // REAKTÍV FIGYELŐ:
+ // Feliratkozunk a menüpontok listájára. Ha a DSpace (pl. kereséskor)
+ // törli a listát, ez a kód azonnal észreveszi és visszateszi a miénket.
+ this.menuSubs.push(
+ this.menuService.getMenuTopSections(MenuID.PUBLIC).subscribe((sections) => {
+ const exists = sections.some(section => section.id === 'elte_graph_viewer');
+ if (!exists) {
+ this.addGraphMenu();
+ }
+ }),
+ );
+ }
+
+ private addGraphMenu() {
+ this.menuService.addSection(MenuID.PUBLIC, {
+ id: 'elte_graph_viewer',
+ active: true,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.graph-viewer',
+ link: '/' + GRAPH_VIEWER_PATH,
+ } as LinkMenuItemModel,
+ icon: 'network-wired',
+ index: 10,
+ });
+ }
+
+ ngOnDestroy() {
+ // Lezárjuk a figyelőt, amikor elhagyjuk az oldalt (memóriaszivárgás ellen)
+ this.menuSubs.forEach(sub => sub.unsubscribe());
+
+ if (super.ngOnDestroy) {
+ super.ngOnDestroy();
+ }
+ }
}
diff --git a/src/themes/elte/assets/i18n/en.json5 b/src/themes/elte/assets/i18n/en.json5
index 39e0cabd1c5..80e9333ee62 100644
--- a/src/themes/elte/assets/i18n/en.json5
+++ b/src/themes/elte/assets/i18n/en.json5
@@ -452,4 +452,8 @@
"footer.instituteName": "Eötvös Loránd University",
"footer.rrf": "The project ID RRF-2.1.2-21-2022-00023, titled 'Infrastructural and skills development of practice-oriented higher education programs', was implemented within the framework of Hungary’s Recovery and Resilience Plan.",
+
+ "graph-viewer.title": "Graph Viewer",
+
+ "menu.section.graph-viewer": "Graph Viewer",
}
diff --git a/src/themes/elte/assets/i18n/hu.json5 b/src/themes/elte/assets/i18n/hu.json5
index 23950987938..5194452917f 100644
--- a/src/themes/elte/assets/i18n/hu.json5
+++ b/src/themes/elte/assets/i18n/hu.json5
@@ -112,7 +112,7 @@
"relationships.isCourseOfLearningObject": "Tantárgyak",
// "relationships.isCourseInstanceOfLearningObject": "Course Instances",
- ""relationships.isCourseInstanceOfLearningObject": "Kurzusok",
+ "relationships.isCourseInstanceOfLearningObject": "Kurzusok",
// "relationships.isTypeOfLearningObject": "Type",
"relationships.isTypeOfLearningObject": "Típus",
@@ -472,6 +472,8 @@
"menu.section.browse_global_by_course": "Tantárgy szerint",
+ "menu.section.graph-viewer": "Gráf megjelenítő",
+
"browse.metadata.program": "Képzés",
"browse.metadata.program.breadcrumbs": "Böngészés képzés szerint",
@@ -496,5 +498,7 @@
"footer.instituteName": "Eötvös Loránd Tudományegyetem",
- "footer.rrf": "Az RRF-2.1.2-21-2022-00023 azonosító számon nyilvántartott „Gyakorlatorientált felsőfokú képzések infrastrukturális- és készségfejlesztése” projekt Magyarország Helyreállítási és Ellenállóképességi Tervének keretében valósult meg."
+ "footer.rrf": "Az RRF-2.1.2-21-2022-00023 azonosító számon nyilvántartott „Gyakorlatorientált felsőfokú képzések infrastrukturális- és készségfejlesztése” projekt Magyarország Helyreállítási és Ellenállóképességi Tervének keretében valósult meg.",
+
+ "graph-viewer.title": "Gráf megjelenítő",
}
diff --git a/src/themes/elte/eager-theme.module.ts b/src/themes/elte/eager-theme.module.ts
index 8a32e9502b8..30520e2c184 100644
--- a/src/themes/elte/eager-theme.module.ts
+++ b/src/themes/elte/eager-theme.module.ts
@@ -8,22 +8,22 @@ import {
} from 'src/app/shared/metadata-representation/metadata-representation.decorator';
import { RootModule } from '../../app/root.module';
-import { GenericItemMetadataListElementComponent } from './app/entity-groups/research-entities/metadata-representations/generic-item/generic-item-metadata-list-element.component';
// Your themed components
import { PersonComponent } from './app/entity-groups/research-entities/item-pages/person/person.component';
+import { GenericItemMetadataListElementComponent } from './app/entity-groups/research-entities/metadata-representations/generic-item/generic-item-metadata-list-element.component';
import { FooterComponent } from './app/footer/footer.component';
import { HeaderComponent } from './app/header/header.component';
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
import { HomeNewsComponent } from './app/home-page/home-news/home-news.component';
import { ElteRelatedItemsComponent } from './app/item-page/simple/elte-related-items/elte-related-items.component';
-import { LearningObjectComponent } from './app/item-page/simple/item-types/learning-object/learning-object.component';
+import { MetadataValuesComponent } from './app/item-page/simple/field-components/specific-field/metadata-values/metadata-values.component';
import { CourseInstanceComponent } from './app/item-page/simple/item-types/course-instance/course-instance.component';
+import { LearningObjectComponent } from './app/item-page/simple/item-types/learning-object/learning-object.component';
import { PublicationComponent } from './app/item-page/simple/item-types/publication/publication.component';
import { SimpleItemComponent } from './app/item-page/simple/item-types/simple-item/simple-item.component';
import { NavbarComponent } from './app/navbar/navbar.component';
import { LangSwitchComponent } from './app/shared/lang-switch/lang-switch.component';
import { CommunityListElementComponent } from './app/shared/object-list/community-list-element/community-list-element.component';
-import { MetadataValuesComponent } from './app/item-page/simple/field-components/specific-field/metadata-values/metadata-values.component';
const THEME = 'elte';