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';